Skip to main content

bloq_compile/
compile.rs

1use std::num::NonZeroUsize;
2use std::sync::{Arc, OnceLock};
3use std::time::Duration;
4
5use bloq_graph::{
6    Basis, Block, BlockGraph, BlockKind, ModuleCertificationError, ModuleCertificationLimits,
7    ModuleSummary, PortRole, RuntimeStabilizerBasis, StabilizerGenerators, default_module_jobs,
8};
9use glam::{IVec2, IVec3};
10use web_time::Instant;
11
12use crate::block::{
13    LoweringTemplateId, SelectiveTemplates, SpatialHadamardKey, WallSide, compile_fixed_bulk,
14    compile_multiplex_port, compile_realignment, compile_selective, compile_spatial_hadamard,
15    validate_fixed_bulk,
16};
17use crate::config::CompileConfig;
18use crate::lower::{PhysicalInput, lower_clifford_proxy};
19use crate::signature::{
20    BlockSignature, BlockSignatureMap, block_signatures, derive_block_signature,
21    join_component_layer_schedules, relocatable_block_signatures,
22};
23use crate::spatial_port::{SpatialPortExpansionMap, expand_spatial_ports};
24use crate::{Bloq, CompileError, Connectivity};
25
26#[path = "registry.rs"]
27mod registry;
28
29/// Compiled template info for every block, keyed by its grid position.
30pub(crate) type CompiledTemplateMap = crate::FxMap<IVec3, CompiledTemplateInfo>;
31type CompiledDefinitionVariantMap =
32    crate::FxMap<IVec3, Vec<(BlockSignature, CompiledTemplateInfo)>>;
33/// Validated semantic input and signatures for the pinned Clifford oracle.
34struct ProxyPreflight {
35    graph: BlockGraph,
36    signatures: BlockSignatureMap,
37    stabilizers: StabilizerGenerators,
38    spatial_ports: SpatialPortExpansionMap,
39}
40
41/// Metadata key for the code distance used to compile a [`Bloq`].
42pub const CODE_DISTANCE_METADATA_KEY: &str = "bloq_compile.code_distance";
43/// Metadata key for the layout convention used to compile a [`Bloq`].
44pub const CONVENTION_METADATA_KEY: &str = "bloq_compile.convention";
45/// Metadata key for the seed used to sample a random Clifford proxy path.
46pub const CLIFFORD_PROXY_SEED_METADATA_KEY: &str = "bloq_compile.clifford_proxy_seed";
47
48/// The `z` shift that `BlockGraph::with_zero_min_z` applies to `graph`.
49///
50/// Module summaries and site relocations are both expressed in the source
51/// module's coordinates, so both must be moved by exactly this offset before
52/// they line up with the normalized graph.
53fn module_normalization_offset(
54    graph: &BlockGraph,
55    module: &ModuleSummary,
56) -> Result<IVec3, CompileError> {
57    let min_z = graph.spans().map_or(0, |(_, _, span)| *span.start());
58    let z = i32::try_from(-i64::from(min_z)).map_err(|_| {
59        ModuleCertificationError::InvalidConnection {
60            module: module.name().to_string(),
61            message: "module normalization exceeds i32 coordinates".to_string(),
62        }
63    })?;
64    Ok(IVec3::new(0, 0, z))
65}
66
67/// Qubit-layout offset of the block at grid position `block_xy`:
68/// `block_xy · (2·distance + 2)`.
69pub(crate) fn block_xy_offset(block_xy: IVec2, distance: u32) -> Result<IVec2, CompileError> {
70    let stride = 2 * i128::from(distance) + 2;
71    let x = i32::try_from(i128::from(block_xy.x) * stride)
72        .map_err(|_| CompileError::BlockLayoutCoordinateOverflow { block_xy, distance })?;
73    let y = i32::try_from(i128::from(block_xy.y) * stride)
74        .map_err(|_| CompileError::BlockLayoutCoordinateOverflow { block_xy, distance })?;
75    Ok(IVec2::new(x, y))
76}
77
78/// Block-grid → qubit-layout mapping: a block at grid `pos` places its template
79/// at `offset(pos) = pos.xy * (2·distance + 2)`.
80#[derive(Debug, Clone, Copy)]
81pub(crate) struct BlockLayout {
82    distance: u32,
83}
84
85impl BlockLayout {
86    pub(crate) fn new(distance: u32) -> Self {
87        Self { distance }
88    }
89
90    pub(crate) fn distance(self) -> u32 {
91        self.distance
92    }
93
94    pub(crate) fn offset(self, pos: IVec3) -> Result<IVec2, CompileError> {
95        block_xy_offset(pos.truncate(), self.distance)
96    }
97}
98
99/// The output of a successful compilation: the lowered [`Bloq`] program, how
100/// long the compile took, and any advisories the compiler raised about it.
101#[derive(Debug, Clone)]
102pub struct CompileArtifacts {
103    /// The compiled backend IR program.
104    pub bloq: Bloq,
105    /// Wall-clock time spent in the compile or link call returning this artifact.
106    pub compile_duration: Duration,
107    /// Advisories about the compiled program: constructions that compile but
108    /// whose circuit does not deliver everything the configuration asks for
109    /// (currently only [`spatial_hadamard_distance_warning`]).
110    ///
111    /// In-band so a front end cannot silently forget to ask — the free
112    /// functions stay available for pre-flight, before a compile exists.
113    ///
114    /// [`spatial_hadamard_distance_warning`]: crate::spatial_hadamard_distance_warning
115    pub warnings: Vec<&'static str>,
116}
117
118/// A compiled block graph for one physical target, ready for repeated linking.
119///
120/// It shares the finished IR and advisories. Linking returns an independently
121/// editable snapshot without repeating certification, template compilation, or
122/// lowering.
123pub struct CompiledObject {
124    config: CompileConfig,
125    artifacts: Arc<CompileArtifacts>,
126}
127
128impl std::fmt::Debug for CompiledObject {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        f.debug_struct("CompiledObject")
131            .field("config", &self.config)
132            .finish_non_exhaustive()
133    }
134}
135
136impl CompiledObject {
137    /// Physical target this object was built for.
138    #[must_use]
139    pub fn config(&self) -> CompileConfig {
140        self.config
141    }
142}
143
144/// Pinned Clifford input to the physical oracle's lowering.
145struct PreparedProxy {
146    graph: BlockGraph,
147    stabilizers: StabilizerGenerators,
148    spatial_ports: SpatialPortExpansionMap,
149    compiled: CompiledTemplateMap,
150    spatial_port_templates: CompiledTemplateMap,
151    plan: crate::lower::LinkedPlanInput,
152    temporal_templates: crate::FxMap<crate::lower::TemplatePlan, LoweringTemplateId>,
153    wall_templates: crate::FxMap<crate::lower::SpatialPipeRef, LoweringTemplateId>,
154    padding_templates: crate::padding::PreparedPaddingTemplates,
155    template_pool: crate::block::LoweringTemplatePool,
156}
157
158pub(crate) struct PreparedDefinitionObject {
159    name: String,
160    dependencies: Vec<Arc<PreparedDefinitionObject>>,
161    variants: CompiledDefinitionVariants,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
165pub(crate) struct DefinitionVariantKey {
166    local_position: IVec3,
167    signature: BlockSignature,
168    graph_connectivity: Connectivity,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Hash)]
172pub(crate) struct DefinitionObjectCacheKey {
173    implementation: String,
174    orientations: Vec<bloq_graph::ModuleOrientation>,
175}
176
177#[derive(Debug, Clone)]
178struct PreparedSiteRelocation {
179    position: IVec3,
180    definition: usize,
181    local_position: IVec3,
182}
183
184type CompiledDefinitionVariants = crate::FxMap<DefinitionVariantKey, CompiledTemplateInfo>;
185fn build_definition_objects(
186    cache: &crate::cache::TemplateCacheShard,
187    names: &[String],
188    mut parts_by_name: std::collections::HashMap<String, CompiledDefinitionVariants>,
189    mut built: std::collections::HashMap<String, Arc<PreparedDefinitionObject>>,
190    definition_keys: &std::collections::HashMap<String, String>,
191    orientations_by_name: &std::collections::HashMap<String, Vec<bloq_graph::ModuleOrientation>>,
192    program: &BlockGraph,
193) -> Vec<Arc<PreparedDefinitionObject>> {
194    while built.len() < names.len() {
195        let before = built.len();
196        for name in names {
197            if built.contains_key(name) {
198                continue;
199            }
200            let definition = program
201                .module(name)
202                .expect("reachable cache keys name validated definitions");
203            let mut dependency_names = definition
204                .instances
205                .iter()
206                .map(|instance| instance.definition.as_str())
207                .collect::<Vec<_>>();
208            dependency_names.sort_unstable();
209            dependency_names.dedup();
210            if dependency_names
211                .iter()
212                .any(|dependency| !built.contains_key(*dependency))
213            {
214                continue;
215            }
216            let dependencies = dependency_names
217                .into_iter()
218                .map(|dependency| Arc::clone(&built[dependency]))
219                .collect();
220            let variants = parts_by_name
221                .remove(name)
222                .expect("every definition has a retained object or compiled parts");
223            let object = PreparedDefinitionObject {
224                name: name.clone(),
225                dependencies,
226                variants,
227            };
228            let object = cache.insert_definition_object(
229                DefinitionObjectCacheKey {
230                    implementation: definition_keys[name].clone(),
231                    orientations: orientations_by_name[name].clone(),
232                },
233                Arc::new(object),
234            );
235            built.insert(name.clone(), object);
236        }
237        assert!(
238            built.len() > before,
239            "validated module dependencies are acyclic"
240        );
241    }
242    let definitions = names
243        .iter()
244        .map(|name| Arc::clone(&built[name]))
245        .collect::<Vec<_>>();
246    debug_assert!(definitions.iter().all(|object| {
247        object
248            .dependencies
249            .windows(2)
250            .all(|pair| pair[0].name < pair[1].name)
251    }));
252    definitions
253}
254
255fn relocate_definition_sites(
256    definitions: &[Arc<PreparedDefinitionObject>],
257    linked_positions: impl IntoIterator<Item = IVec3>,
258    module_sites: &std::collections::HashMap<IVec3, bloq_graph::MaterializedModuleSite>,
259) -> Vec<PreparedSiteRelocation> {
260    let definition_by_name = definitions
261        .iter()
262        .enumerate()
263        .map(|(index, definition)| (definition.name.as_str(), index))
264        .collect::<std::collections::HashMap<_, _>>();
265    let mut relocations = linked_positions
266        .into_iter()
267        .map(|position| {
268            let site = &module_sites[&position];
269            PreparedSiteRelocation {
270                position,
271                definition: definition_by_name[site.definition.as_str()],
272                local_position: site.local_position,
273            }
274        })
275        .collect::<Vec<_>>();
276    relocations.sort_unstable_by_key(|site| site.position.to_array());
277    relocations
278}
279
280fn select_link_signatures(
281    config: CompileConfig,
282    graph: &BlockGraph,
283    spatial_ports: &SpatialPortExpansionMap,
284    #[cfg(test)] builds: &std::sync::atomic::AtomicUsize,
285) -> Result<BlockSignatureMap, CompileError> {
286    #[cfg(test)]
287    builds.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
288    let schedules = join_component_layer_schedules(graph)?;
289    let mut signatures = block_signatures(graph, &schedules, config.code_distance())?;
290    for expansion in spatial_ports.values() {
291        let signature = signatures
292            .get_mut(&expansion.source)
293            .expect("signature map contains the derived cube");
294        signature.connectivity = signature.connectivity.with_pipe(expansion.cube_pipe_dir());
295    }
296    for &signature in signatures.values() {
297        validate_fixed_bulk(signature)?;
298    }
299    Ok(signatures)
300}
301
302fn select_module_link_templates_with_schedule(
303    config: CompileConfig,
304    graph: &BlockGraph,
305    spatial_ports: &SpatialPortExpansionMap,
306    definitions: &[Arc<PreparedDefinitionObject>],
307    sites: &[PreparedSiteRelocation],
308    schedules: &crate::signature::LayerScheduleMap,
309    t_sides: &crate::FxMap<IVec3, bloq_graph::Direction>,
310) -> Result<CompiledTemplateMap, CompileError> {
311    sites
312        .iter()
313        .map(|site| {
314            let definition = &definitions[site.definition];
315            let block = graph
316                .get_block(site.position)
317                .expect("module relocation names a linked block");
318            let mut graph_connectivity = BlockSignature::graph_connectivity(block, graph);
319            if let Some(expansion) = spatial_ports.get(&site.position) {
320                graph_connectivity = graph_connectivity.with_pipe(expansion.cube_pipe_dir());
321            }
322            let surgery_side = (block.kind() == BlockKind::T).then(|| t_sides[&site.position]);
323            let mut linked_signature = derive_block_signature(
324                graph,
325                block,
326                config.code_distance(),
327                crate::signature::layer_schedule_at(schedules, site.position),
328                surgery_side,
329            )?;
330            if let Some(expansion) = spatial_ports.get(&site.position) {
331                linked_signature.connectivity = linked_signature
332                    .connectivity
333                    .with_pipe(expansion.cube_pipe_dir());
334            }
335            let variant = DefinitionVariantKey {
336                local_position: site.local_position,
337                signature: linked_signature,
338                graph_connectivity,
339            };
340            let Some(info) = definition.variants.get(&variant) else {
341                return Err(CompileError::MissingModulePhysicalVariant {
342                    module: definition.name.clone(),
343                    local_position: site.local_position,
344                    linked_position: site.position,
345                    signature: format!(
346                        "signature={linked_signature:?}, graph={graph_connectivity:?}"
347                    ),
348                });
349            };
350            validate_fixed_bulk(variant.signature)?;
351            Ok((site.position, *info))
352        })
353        .collect()
354}
355
356/// Compile `graph` at `distance`, returning the lowered program.
357///
358/// The one-shot for the common case: a fresh private template cache, no
359/// artifacts wrapper, and a validated distance (no panic on user input). Reach
360/// for [`CompileContext`] when several compilations should share compiled
361/// templates, and for [`compile_with`] when the full [`CompileArtifacts`] are
362/// wanted.
363///
364/// # Errors
365///
366/// Returns [`CompileError::InvalidDistance`] if `distance` is not odd and in
367/// `3..=`[`MAX_CODE_DISTANCE`](crate::MAX_CODE_DISTANCE), and any compilation
368/// error the graph provokes.
369///
370/// # Examples
371///
372/// ```
373/// use bloq_compile::compile;
374/// use bloq_graph::GalleryItem;
375///
376/// let bloq = compile(&GalleryItem::CNOT.build(), 3)?;
377/// assert!(bloq.quantum_node_count() > 0);
378/// # Ok::<(), bloq_compile::CompileError>(())
379/// ```
380pub fn compile(graph: &BlockGraph, distance: u32) -> Result<Bloq, CompileError> {
381    let config = CompileConfig::try_new(distance)?;
382    Ok(compile_with(graph, config)?.bloq)
383}
384
385/// Compile `graph` under `config` in a fresh context, returning the full
386/// [`CompileArtifacts`].
387///
388/// # Errors
389///
390/// Any compilation error the graph provokes under `config`.
391///
392/// # Examples
393///
394/// ```
395/// use bloq_compile::{CompileConfig, compile_with};
396/// use bloq_graph::GalleryItem;
397///
398/// let artifacts = compile_with(&GalleryItem::CNOT.build(), CompileConfig::default())?;
399/// assert!(artifacts.warnings.is_empty());
400/// # Ok::<(), bloq_compile::CompileError>(())
401/// ```
402pub fn compile_with(
403    graph: &BlockGraph,
404    config: CompileConfig,
405) -> Result<CompileArtifacts, CompileError> {
406    CompileContext::new(config).compile(graph)
407}
408
409/// The code distance `bloq` was compiled at, or `None` if it carries no
410/// [`CODE_DISTANCE_METADATA_KEY`] entry (it was not produced by this compiler,
411/// or predates the key).
412#[must_use]
413pub fn compiled_distance(bloq: &Bloq) -> Option<u32> {
414    match bloq.metadata().get(CODE_DISTANCE_METADATA_KEY)? {
415        bloq_ir::MetadataValue::U64(distance) => u32::try_from(*distance).ok(),
416        bloq_ir::MetadataValue::String(_) => None,
417    }
418}
419
420/// A compiled block's [`TemplateRef`] together with the graph connectivity it
421/// was compiled for.
422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
423pub(crate) struct CompiledTemplateInfo {
424    pub(crate) graph_connectivity: Connectivity,
425    pub(crate) template: TemplateRef,
426}
427
428/// How a compiled block's template(s) live in the pool.
429///
430/// A fixed block has one template. A selective block has two measurement arms —
431/// one per resolved Pauli basis — in one guarded component.
432/// Lowering classifies each plan node once (`lower::classify_nodes`), so the
433/// emission passes never re-derive this.
434/// A T block has its cultivation + escape stage pair; lowering turns
435/// these into a `RepeatUntilSuccess` region whose escape instance is the
436/// block's observable-facing template.
437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
438pub(crate) enum TemplateRef {
439    Fixed(LoweringTemplateId),
440    Selective {
441        when_true: LoweringTemplateId,
442        when_false: LoweringTemplateId,
443    },
444    T {
445        cultivation: LoweringTemplateId,
446        escape: LoweringTemplateId,
447    },
448}
449
450impl TemplateRef {
451    /// Whether this block lowers to a guarded selective pair. Static observable
452    /// lookup skips these; guarded lookup resolves each arm separately.
453    pub(crate) fn is_selective(self) -> bool {
454        matches!(self, TemplateRef::Selective { .. })
455    }
456
457    /// The template the block's observable gateway lives on: the fixed template,
458    /// or a T block's escape template (its output face is the escaped patch).
459    /// `None` for selectives, whose observables resolve per pinned arm.
460    pub(crate) fn observable_template(self) -> Option<LoweringTemplateId> {
461        match self {
462            TemplateRef::Fixed(id) => Some(id),
463            TemplateRef::T { escape, .. } => Some(escape),
464            TemplateRef::Selective { .. } => None,
465        }
466    }
467}
468
469/// A stage entered by a compilation. Stages can repeat if a planner retries.
470#[derive(Debug, Clone, Copy, PartialEq, Eq)]
471pub enum CompileStage {
472    /// Validate the source and target limits.
473    Validation,
474    /// Certify module definitions.
475    Certification,
476    /// Build the logical correlation model.
477    Correlations,
478    /// Schedule the physical geometry.
479    Placement,
480    /// Compile templates, bind physical instances, and compose flows.
481    Templates,
482    /// Bind physical measurements to logical readouts.
483    Readouts,
484    /// Add occupancy order, optimize IR, bind logical cuts, and check layout.
485    Optimization,
486    /// Use a previously compiled module object.
487    CacheReuse,
488    /// Compilation succeeded.
489    Complete,
490}
491
492impl std::fmt::Display for CompileStage {
493    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
494        formatter.write_str(match self {
495            Self::Validation => "Validating source",
496            Self::Certification => "Certifying module",
497            Self::Correlations => "Planning correlations",
498            Self::Placement => "Placing physical blocks",
499            Self::Templates => "Binding physical program",
500            Self::Readouts => "Lowering readouts",
501            Self::Optimization => "Finalizing program",
502            Self::CacheReuse => "Reusing cached module",
503            Self::Complete => "Compilation complete",
504        })
505    }
506}
507
508struct ProgressObserver(Box<dyn Fn(CompileStage) + Send + Sync>);
509
510impl std::fmt::Debug for ProgressObserver {
511    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
512        formatter.write_str("ProgressObserver(..)")
513    }
514}
515
516/// The main compilation context. Orchestrates the full pipeline:
517/// BlockGraph -> Bloq.
518///
519/// Compiled circuit templates live in the context's cache shard, which
520/// [`new`](Self::new) keeps private and
521/// [`with_shared_cache`](Self::with_shared_cache) shares with other contexts —
522/// see [`SharedCompileCache`](crate::SharedCompileCache).
523#[derive(Debug)]
524pub struct CompileContext {
525    config: CompileConfig,
526    cache: Arc<crate::cache::TemplateCacheShard>,
527    summaries: Arc<crate::cache::ModuleSummaryCache>,
528    clifford_proxy: Option<CliffordProxyPins>,
529    progress_observer: Option<ProgressObserver>,
530    cancellation: Option<bloq_graph::CancellationToken>,
531    #[cfg(test)]
532    flat_signature_builds: std::sync::atomic::AtomicUsize,
533}
534
535/// Per-site branch pins for the Clifford-proxy distance oracle:
536/// one bit per selective block, consumed in [`ordered_blocks`] order.
537#[derive(Debug)]
538struct CliffordProxyPins {
539    pins: Vec<bool>,
540    purpose: CliffordProxyPurpose,
541    source_summary: Option<Arc<ModuleSummary>>,
542}
543
544/// Which consumer requested a pinned Clifford artifact.
545#[derive(Debug, Clone, Copy, PartialEq, Eq)]
546enum CliffordProxyPurpose {
547    /// Static distance oracle: replace a T region with a perfect input port.
548    Distance,
549    /// Detector-slice viewer: retain the T region so its physical timeline is
550    /// inspectable; the slice tracker applies each T gate's Clifford proxy.
551    Detslice,
552}
553
554/// Compile `graph` with every dynamic element replaced by a Clifford stand-in.
555///
556/// This is a **distance oracle, not a semantics oracle**. Exact graph-level ZX
557/// maps remain the logical-correctness oracle:
558///
559/// - Every T block becomes a perfect MPP **input port** on its escaped-patch
560///   footprint: a noiseless preparation of the `d×d` patch (one MPP record
561///   per stabilizer, closing the +Z neighbour's seam like any memory face)
562///   whose passthrough gateway anchors a terminating logical at the past
563///   boundary via the ordinary boundary `OBSERVABLE_INCLUDE` MPP. A logical
564///   `Y` request resolves through the gateway's X·Z composition — msc-ls's
565///   S-for-T substitution lifted from per-gate to whole-block; the magic
566///   state's phase does not affect circuit distance. The cultivation/escape
567///   circuits are dropped wholesale (the standalone harness owns their
568///   internal distance).
569/// - Every selective block is statically pinned to one arm per `pins` bit
570///   (deterministic `ordered_blocks` order), eliminating the dynamic choice
571///   dispatch; sweeping pin combos covers the branch space.
572/// - Structural `Branch` actions are rejected: these pins describe selective
573///   arms only and cannot choose an authored graph region.
574/// - The action track (feedback, conditional-S corrections) is dropped —
575///   Clifford frame corrections cannot change circuit distance.
576/// - Hierarchical inputs reuse their composed source certificate; topology is
577///   expanded only for this physical oracle.
578/// - Source stabilizer rows are reused and resolved through the pinned fills,
579///   without re-deriving a basis from the rewritten graph. Output frames use
580///   those resolved rows without replaying source actions.
581///
582/// The result is a fully determined program the static Stim backend accepts,
583/// suitable for `shortest_graphlike_error` / undetectable-error searches.
584///
585/// # Errors
586///
587/// Returns [`CompileError::CliffordProxyPinCount`] unless `pins` has exactly one
588/// bit per selective block in the expanded graph, and any compilation error the
589/// rewritten graph provokes. Pins follow expanded `(z, x, y)` block order.
590///
591/// # Examples
592///
593/// ```
594/// use bloq_compile::{CompileConfig, compile_clifford_proxy};
595/// use bloq_graph::GalleryItem;
596///
597/// // One pin per selective block; the T gallery graph has one.
598/// let proxy = compile_clifford_proxy(CompileConfig::default(), &GalleryItem::T.build(), &[false])?;
599/// // Fully Clifford, so the static Stim backend accepts it.
600/// bloq_stim::emit_bloq_stim(&proxy.bloq).expect("Clifford proxy emits static Stim");
601/// # Ok::<(), bloq_compile::CompileError>(())
602/// ```
603pub fn compile_clifford_proxy(
604    config: CompileConfig,
605    graph: &BlockGraph,
606    pins: &[bool],
607) -> Result<CompileArtifacts, CompileError> {
608    compile_proxy(config, graph, pins, CliffordProxyPurpose::Distance)
609}
610
611/// Compile one seeded random static Clifford proxy path.
612///
613/// Shared selective controls are sampled jointly. The seed is stored under
614/// [`CLIFFORD_PROXY_SEED_METADATA_KEY`]; T blocks use the usual proxy ports.
615/// Structural branches are rejected before random selective resolution drops
616/// the source action track.
617///
618/// # Errors
619///
620/// Returns graph validation, proxy selection, or compilation errors.
621pub fn compile_random_clifford_proxy(
622    config: CompileConfig,
623    graph: &BlockGraph,
624    seed: u64,
625) -> Result<CompileArtifacts, CompileError> {
626    let flat = flat_proxy_graph(config, graph)?;
627    let source = flat.as_ref().unwrap_or(graph);
628    let pins = random_proxy_pins(source, seed)?;
629    let mut artifacts = compile_pinned_clifford_proxy(
630        config,
631        source,
632        &pins,
633        CliffordProxyPurpose::Distance,
634        graph.has_module_structure().then_some(graph),
635    )?;
636    artifacts.bloq.insert_metadata(
637        CLIFFORD_PROXY_SEED_METADATA_KEY,
638        bloq_ir::MetadataValue::U64(seed),
639    );
640    Ok(artifacts)
641}
642
643fn random_proxy_pins(graph: &BlockGraph, seed: u64) -> Result<Vec<bool>, CompileError> {
644    // Random projection drops every action, so reject unsupported structural
645    // composition before that rewrite can hide the Branch source.
646    reject_structural_proxy(graph)?;
647    let (_, replacements) = graph.randomly_resolve_selectives(seed)?;
648    let pins = ordered_blocks(graph)
649        .into_iter()
650        .filter_map(|block| {
651            let BlockKind::Selective(kind) = block.kind() else {
652                return None;
653            };
654            let resolved = replacements
655                .get(block)
656                .expect("random resolution replaces every selective block");
657            let chosen = match resolved.kind() {
658                BlockKind::Y => bloq_graph::PauliBasis::Y,
659                BlockKind::Measurement(basis) => basis.into(),
660                _ => unreachable!("a resolved selective is a Y or measurement block"),
661            };
662            Some(chosen == kind.pauli_if_true())
663        })
664        .collect::<Vec<_>>();
665    Ok(pins)
666}
667
668/// Compile a pinned detector-slice proxy.
669///
670/// Selectives resolve the source stabilizer basis, while T regions remain for
671/// display and are tracked with the slice engine's T-to-S substitution.
672///
673/// # Errors
674///
675/// Returns [`CompileError::CliffordProxyPinCount`] unless `pins` has exactly one
676/// bit per selective block in the expanded graph, and any compilation error the
677/// rewritten graph provokes. Pins follow expanded `(z, x, y)` block order.
678pub fn compile_detslice_proxy(
679    config: CompileConfig,
680    graph: &BlockGraph,
681    pins: &[bool],
682) -> Result<CompileArtifacts, CompileError> {
683    compile_proxy(config, graph, pins, CliffordProxyPurpose::Detslice)
684}
685
686fn flat_proxy_graph(
687    config: CompileConfig,
688    graph: &BlockGraph,
689) -> Result<Option<BlockGraph>, CompileError> {
690    if !graph.has_module_structure() {
691        return Ok(None);
692    }
693    Ok(Some(
694        graph.flatten_with_limits(config.certification_limits())?,
695    ))
696}
697
698fn compile_proxy(
699    config: CompileConfig,
700    graph: &BlockGraph,
701    pins: &[bool],
702    purpose: CliffordProxyPurpose,
703) -> Result<CompileArtifacts, CompileError> {
704    let flat = flat_proxy_graph(config, graph)?;
705    compile_pinned_clifford_proxy(
706        config,
707        flat.as_ref().unwrap_or(graph),
708        pins,
709        purpose,
710        graph.has_module_structure().then_some(graph),
711    )
712}
713
714fn compile_pinned_clifford_proxy(
715    config: CompileConfig,
716    graph: &BlockGraph,
717    pins: &[bool],
718    purpose: CliffordProxyPurpose,
719    program: Option<&BlockGraph>,
720) -> Result<CompileArtifacts, CompileError> {
721    let expected = graph
722        .blocks()
723        .filter(|block| block.kind().is_selective())
724        .count();
725    if pins.len() != expected {
726        return Err(CompileError::CliffordProxyPinCount {
727            expected,
728            actual: pins.len(),
729        });
730    }
731    static CACHE: OnceLock<crate::SharedCompileCache> = OnceLock::new();
732    // Keep proxy templates separate from real compilations. Pinning only
733    // selects one arm of a cached selective template; T proxy ports use their
734    // ordinary Port signature below, so no cached entry is site-specific.
735    let mut ctx = CompileContext::with_shared_cache(
736        config,
737        CACHE.get_or_init(crate::SharedCompileCache::new),
738    );
739    let source_summary = program
740        .map(|program| ctx.summarize(program, config.certification_limits()))
741        .transpose()?;
742    ctx.clifford_proxy = Some(CliffordProxyPins {
743        pins: pins.to_vec(),
744        purpose,
745        source_summary,
746    });
747    ctx.compile(graph)
748}
749
750/// Cache key for the unified signature→template map: a spatial block signature,
751/// a temporal Hadamard realignment keyed by its top basis, or a spatial Hadamard
752/// wall keyed by its two endpoint cubes' contributions. Collapsing them into one
753/// enum lets `sig_to_template` back every template kind.
754#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
755pub(crate) enum Signature {
756    Block(BlockSignature),
757    MultiplexPort {
758        boundary_basis: Basis,
759        output_qubit: IVec2,
760    },
761    Realignment {
762        top_basis: Basis,
763    },
764    SpatialHadamardPipe(SpatialHadamardKey),
765}
766
767impl CompileContext {
768    /// Creates a context with a private template cache. Templates it compiles
769    /// are reused by its own later compilations and by nothing else.
770    #[must_use]
771    pub fn new(config: CompileConfig) -> Self {
772        Self {
773            config,
774            cache: Arc::new(crate::cache::TemplateCacheShard::default()),
775            summaries: Arc::new(crate::cache::ModuleSummaryCache::default()),
776            clifford_proxy: None,
777            progress_observer: None,
778            cancellation: None,
779            #[cfg(test)]
780            flat_signature_builds: std::sync::atomic::AtomicUsize::new(0),
781        }
782    }
783
784    /// Creates a context backed by `cache`, so it reuses (and contributes)
785    /// templates compiled by every other context sharing that cache at this
786    /// [`CompileConfig`]. Contexts sharing a cache may compile concurrently.
787    ///
788    /// Sharing does not change compiled output: see [`SharedCompileCache`].
789    ///
790    /// [`SharedCompileCache`]: crate::SharedCompileCache
791    #[must_use]
792    pub fn with_shared_cache(config: CompileConfig, cache: &crate::SharedCompileCache) -> Self {
793        Self {
794            config,
795            cache: cache.shard(config),
796            summaries: cache.summary_cache(),
797            clifford_proxy: None,
798            progress_observer: None,
799            cancellation: None,
800            #[cfg(test)]
801            flat_signature_builds: std::sync::atomic::AtomicUsize::new(0),
802        }
803    }
804
805    /// The configuration this context compiles at.
806    ///
807    /// Front ends that display or re-derive a context's settings read them
808    /// here instead of keeping their own copy of what was passed to
809    /// [`new`](Self::new).
810    #[must_use]
811    pub fn config(&self) -> CompileConfig {
812        self.config
813    }
814
815    /// Observe stage starts for compilations made with this context.
816    /// The default context emits no events. Retries may repeat stages; these
817    /// events are not a completion fraction. The callback runs synchronously
818    /// and should stay cheap. Concurrent calls on one context may interleave.
819    #[must_use]
820    pub fn with_progress_observer(
821        mut self,
822        observer: impl Fn(CompileStage) + Send + Sync + 'static,
823    ) -> Self {
824        self.progress_observer = Some(ProgressObserver(Box::new(observer)));
825        self
826    }
827
828    /// Makes this context's compilations cooperatively cancellable.
829    ///
830    /// Cancellation is scoped to this context's request, including native module
831    /// workers. Checks occur at stage, template, and normalization boundaries;
832    /// no partial artifact is returned. Cached complete templates remain reusable.
833    /// Use a new token for a later request.
834    #[must_use]
835    pub fn with_cancellation(mut self, token: bloq_graph::CancellationToken) -> Self {
836        self.cancellation = Some(token);
837        self
838    }
839
840    fn check_cancellation(&self) -> Result<(), CompileError> {
841        if let Some(token) = &self.cancellation {
842            token.check()?;
843        }
844        Ok(())
845    }
846
847    fn cancellable<T>(
848        &self,
849        f: impl FnOnce() -> Result<T, CompileError>,
850    ) -> Result<T, CompileError> {
851        match &self.cancellation {
852            Some(token) => token.run(f),
853            None => f(),
854        }
855    }
856
857    pub(crate) fn report_progress(&self, stage: CompileStage) -> Result<(), CompileError> {
858        self.check_cancellation()?;
859        if let Some(observer) = &self.progress_observer {
860            (observer.0)(stage);
861        }
862        self.check_cancellation()
863    }
864
865    /// Certifies and summarizes a module hierarchy, reusing this context's
866    /// logical cache independently of physical compile configuration.
867    ///
868    /// # Errors
869    ///
870    /// Returns module validation, resource-limit, or certification errors.
871    pub fn summarize(
872        &self,
873        program: &BlockGraph,
874        limits: ModuleCertificationLimits,
875    ) -> Result<Arc<ModuleSummary>, ModuleCertificationError> {
876        self.summaries.get_or_summarize(program, limits, None)
877    }
878
879    /// [`summarize`](Self::summarize) with an explicit native
880    /// module-worker limit. WebAssembly remains serial.
881    ///
882    /// # Errors
883    ///
884    /// Returns module validation, resource-limit, or certification errors.
885    pub fn summarize_with_jobs(
886        &self,
887        program: &BlockGraph,
888        limits: ModuleCertificationLimits,
889        jobs: NonZeroUsize,
890    ) -> Result<Arc<ModuleSummary>, ModuleCertificationError> {
891        self.summaries.get_or_summarize(program, limits, Some(jobs))
892    }
893
894    /// Compile a target-specific block-graph object for repeated linking.
895    /// Both fixed and symbolic inputs cache their finished IR. Exact
896    /// definition variants remain reusable across different root programs.
897    ///
898    /// # Errors
899    ///
900    /// Returns [`CompileError`] when module validation, certification, or
901    /// lowering fails.
902    pub fn compile_object(&self, program: &BlockGraph) -> Result<CompiledObject, CompileError> {
903        let object = self.compile_object_with_jobs(program, default_module_jobs())?;
904        self.report_progress(CompileStage::Complete)?;
905        Ok(object)
906    }
907
908    fn compile_object_with_jobs(
909        &self,
910        program: &BlockGraph,
911        jobs: NonZeroUsize,
912    ) -> Result<CompiledObject, CompileError> {
913        self.cancellable(|| {
914            self.report_progress(CompileStage::Validation)?;
915            let hierarchical = program.has_module_structure();
916            if hierarchical {
917                program.validate_with_limits(self.config.certification_limits())?;
918            } else {
919                program.validate_resource_limits(self.config.certification_limits())?;
920            }
921            let key = (hierarchical, program.to_blog_text());
922            let artifacts = if let Some(cached) = self.cache.module_object(&key) {
923                self.report_progress(CompileStage::CacheReuse)?;
924                cached
925            } else {
926                self.report_progress(CompileStage::Certification)?;
927                let artifacts = if hierarchical {
928                    registry::compile_hierarchy(self, program, jobs)?
929                } else {
930                    registry::compile_graph(self, program)?
931                };
932                self.check_cancellation()?;
933                self.cache.insert_module_object(key, Arc::new(artifacts))
934            };
935            Ok(CompiledObject {
936                config: self.config,
937                artifacts,
938            })
939        })
940    }
941
942    fn prepare_module_definitions(
943        &self,
944        program: &BlockGraph,
945        jobs: NonZeroUsize,
946    ) -> Result<
947        (
948            bloq_graph::LinkedModuleDefinition,
949            Vec<Arc<PreparedDefinitionObject>>,
950        ),
951        CompileError,
952    > {
953        let (definition_keys, seam_faces) = (
954            program.module_definition_cache_keys(),
955            program.module_public_seam_faces(),
956        );
957        let mut linked = bloq_graph::flatten_module_definition(program, program.root(), "")
958            .map_err(|source| ModuleCertificationError::Graph {
959                module: program.root().name.clone(),
960                source,
961            })?;
962        linked.graph = linked.graph.fix_shadowed_faces();
963        let definitions = self.compile_module_definition_objects(
964            program,
965            &linked,
966            &definition_keys,
967            &seam_faces,
968            jobs,
969        )?;
970        Ok((linked, definitions))
971    }
972
973    /// Link a compiled object to an independently editable Bloq snapshot.
974    ///
975    /// # Errors
976    ///
977    /// Returns a target mismatch. Compilation errors are reported when building
978    /// the object.
979    pub fn link_object(&self, object: &CompiledObject) -> Result<CompileArtifacts, CompileError> {
980        self.check_cancellation()?;
981        if object.config != self.config {
982            return Err(CompileError::CompiledObjectTargetMismatch {
983                object: Box::new(object.config),
984                linker: Box::new(self.config),
985            });
986        }
987        let started_at = Instant::now();
988        let mut artifacts = object.artifacts.as_ref().clone();
989        artifacts.compile_duration = started_at.elapsed();
990        self.check_cancellation()?;
991        Ok(artifacts)
992    }
993
994    /// Compile `graph`, then explicitly audit the resulting Bloq IR.
995    /// The audit inherits this context's Boolean limits; its other resource
996    /// limits retain the IR validator's defaults.
997    ///
998    /// # Errors
999    ///
1000    /// Returns a compilation or full IR validation error.
1001    ///
1002    /// # Examples
1003    ///
1004    /// ```
1005    /// use bloq_compile::{CompileConfig, CompileContext};
1006    /// use bloq_graph::GalleryItem;
1007    ///
1008    /// let context = CompileContext::new(CompileConfig::default());
1009    /// let artifacts = context.compile_and_validate(&GalleryItem::CNOT.build())?;
1010    /// assert!(artifacts.bloq.quantum_node_count() > 0);
1011    /// # Ok::<(), bloq_compile::CompileError>(())
1012    /// ```
1013    pub fn compile_and_validate(
1014        &self,
1015        graph: &BlockGraph,
1016    ) -> Result<CompileArtifacts, CompileError> {
1017        let artifacts = self.compile(graph)?;
1018        let options = bloq_ir::lowering::InstantiationOptions::default()
1019            .with_boolean_limits(self.config.certification_limits().boolean_limits());
1020        artifacts
1021            .bloq
1022            .validate_with_options(&options)
1023            .map_err(CompileError::from_verification_audit)?;
1024        Ok(artifacts)
1025    }
1026
1027    /// Validate the pinned oracle's source before omitting its action track.
1028    fn preflight_proxy(&self, graph: &BlockGraph) -> Result<ProxyPreflight, CompileError> {
1029        graph.validate_resource_limits(self.config.certification_limits())?;
1030        let proxy = self
1031            .clifford_proxy
1032            .as_ref()
1033            .expect("Clifford proxy context");
1034        let module_summary = proxy.source_summary.as_deref();
1035        let graph = graph.canonical_true_branch_view()?;
1036        graph.validate_source()?;
1037        reject_structural_proxy(&graph)?;
1038        let linked_offset = module_summary
1039            .map(|summary| module_normalization_offset(&graph, summary))
1040            .transpose()?
1041            .unwrap_or(IVec3::ZERO);
1042        let graph = graph.with_zero_min_z()?.fix_shadowed_faces();
1043        let (source_graph, mut stabilizers) = if let Some(summary) = module_summary {
1044            let zx = bloq_graph::ZXGraph::from_block_graph_for_analysis(&graph)
1045                .map_err(bloq_graph::RuntimeBasisError::from)?;
1046            let stabilizers = summary.materialize_stabilizers(&zx, linked_offset)?;
1047            (graph.with_analyzed_action_graph(&stabilizers)?, stabilizers)
1048        } else {
1049            graph.analyze_actions_with_limits(self.config.certification_limits())?
1050        };
1051        let (graph, spatial_ports) =
1052            expand_spatial_ports(&source_graph, self.config.code_distance(), &stabilizers)?;
1053        // Module materialization already certifies this on its composed rows.
1054        if module_summary.is_none() {
1055            stabilizers.validate_measurements_close_before_outputs_with_limits(
1056                self.config.certification_limits(),
1057            )?;
1058        }
1059        let changed = stabilizers.prepare_readouts(
1060            source_graph.action_graph(),
1061            self.config.certification_limits(),
1062        )?;
1063        let graph = if changed {
1064            graph.with_analyzed_action_graph(&stabilizers)?
1065        } else {
1066            graph
1067        };
1068        let stabilizers = self.proxy_stabilizers(&graph, &stabilizers)?;
1069        let signatures = select_link_signatures(
1070            self.config,
1071            &graph,
1072            &spatial_ports,
1073            #[cfg(test)]
1074            &self.flat_signature_builds,
1075        )?;
1076        Ok(ProxyPreflight {
1077            graph,
1078            signatures,
1079            stabilizers,
1080            spatial_ports,
1081        })
1082    }
1083
1084    /// Compile a block graph, including its module hierarchy, into Bloq IR.
1085    ///
1086    /// Takes `&self`: the template cache is internally synchronized, so
1087    /// contexts sharing a [`SharedCompileCache`](crate::SharedCompileCache) may
1088    /// compile concurrently from `&`-borrows.
1089    ///
1090    /// # Errors
1091    ///
1092    /// Any structural, per-block, or lowering failure `graph` provokes under
1093    /// this context's [`CompileConfig`].
1094    pub fn compile(&self, graph: &BlockGraph) -> Result<CompileArtifacts, CompileError> {
1095        self.cancellable(|| {
1096            let artifacts = if self.clifford_proxy.is_none() {
1097                if graph.has_module_structure() {
1098                    let started_at = Instant::now();
1099                    let object = self.compile_object_with_jobs(graph, default_module_jobs())?;
1100                    let mut artifacts = self.link_object(&object)?;
1101                    artifacts.compile_duration = started_at.elapsed();
1102                    artifacts
1103                } else {
1104                    registry::compile_graph(self, graph)?
1105                }
1106            } else {
1107                let started_at = Instant::now();
1108                self.report_progress(CompileStage::Validation)?;
1109                let prepared = self.prepare_proxy(graph)?;
1110                self.report_progress(CompileStage::Readouts)?;
1111                let mut artifacts = Self::lower_prepared_proxy(self.config, prepared)?;
1112                artifacts.compile_duration = started_at.elapsed();
1113                artifacts
1114            };
1115            self.report_progress(CompileStage::Complete)?;
1116            Ok(artifacts)
1117        })
1118    }
1119
1120    fn compile_module_definition_objects(
1121        &self,
1122        program: &BlockGraph,
1123        linked_root: &bloq_graph::LinkedModuleDefinition,
1124        definition_keys: &std::collections::HashMap<String, String>,
1125        seam_faces: &std::collections::HashMap<(String, IVec3), Vec<bloq_graph::Direction>>,
1126        jobs: NonZeroUsize,
1127    ) -> Result<Vec<Arc<PreparedDefinitionObject>>, CompileError> {
1128        let orientations_by_name = program.definition_orientations();
1129        let mut names = definition_keys.keys().cloned().collect::<Vec<_>>();
1130        names.sort_unstable();
1131        // Retain hits until assembly: another context may clear the shared
1132        // cache while missing definitions are compiled.
1133        let mut cached = std::collections::HashMap::new();
1134        let mut missing_names = Vec::new();
1135        for name in &names {
1136            let key = DefinitionObjectCacheKey {
1137                implementation: definition_keys[name].clone(),
1138                orientations: orientations_by_name[name].clone(),
1139            };
1140            if let Some(object) = self.cache.definition_object(&key) {
1141                cached.insert(name.clone(), object);
1142            } else {
1143                missing_names.push(name);
1144            }
1145        }
1146        let compile_one = |name: &String| -> Result<CompiledDefinitionVariants, CompileError> {
1147            self.check_cancellation()?;
1148            // Cache hits retain their complete definition object. Materialize
1149            // descendant geometry only for a missing definition, once per build.
1150            let mut linked_child;
1151            let linked = if name == BlockGraph::ENTRY_MODULE {
1152                linked_root
1153            } else {
1154                let definition = program.module(name).expect("validated definition");
1155                linked_child = bloq_graph::flatten_module_definition(program, definition, "")
1156                    .map_err(|source| ModuleCertificationError::Graph {
1157                        module: program.root().name.clone(),
1158                        source,
1159                    })?;
1160                linked_child.graph = linked_child.graph.fix_shadowed_faces();
1161                &linked_child
1162            };
1163            let mut variants = crate::FxMap::default();
1164            for &orientation in &orientations_by_name[name] {
1165                self.check_cancellation()?;
1166                let oriented_graph = linked.graph.with_orientation_lenient(orientation)?;
1167                let oriented_sites = linked
1168                    .sites
1169                    .iter()
1170                    .map(|(&position, site)| {
1171                        let mut site = site.clone();
1172                        site.orientation = orientation.then_orientation(site.orientation);
1173                        orientation
1174                            .try_rotate_position(position)
1175                            .map(|position| (position, site))
1176                    })
1177                    .collect::<Result<std::collections::HashMap<_, _>, _>>()?;
1178                // Native lowering plans the root's readouts again. Child
1179                // interfaces still need their own C0 proof before linking.
1180                if name != BlockGraph::ENTRY_MODULE {
1181                    registry::certify_definition(
1182                        name,
1183                        &oriented_graph,
1184                        &oriented_sites,
1185                        self.config.certification_limits(),
1186                    )?;
1187                }
1188                let graph = oriented_graph.canonical_true_branch_view()?;
1189                let graph = graph.fix_shadowed_faces();
1190                let (graph, spatial_ports) = crate::spatial_port::expand_spatial_port_topology(
1191                    &graph,
1192                    self.config.code_distance(),
1193                )?;
1194                let local_positions = oriented_sites
1195                    .iter()
1196                    .filter(|(_, site)| site.definition == *name && site.instance_path.is_empty())
1197                    .map(|(&position, _)| position)
1198                    .collect::<crate::FxSet<_>>();
1199                let signatures = relocatable_block_signatures(
1200                    &graph,
1201                    &local_positions,
1202                    self.config.code_distance(),
1203                )?;
1204                let mut inventories = vec![self.compile_definition_variants(
1205                    &graph,
1206                    &signatures,
1207                    &oriented_sites,
1208                    seam_faces,
1209                    &spatial_ports,
1210                )?];
1211                let regions = graph.branch_regions()?;
1212                if !regions.is_empty() {
1213                    let projected = graph.project_branches_deferred(
1214                        regions.iter().map(|region| (region.target, false)),
1215                    )?;
1216                    let signatures = relocatable_block_signatures(
1217                        &projected,
1218                        &local_positions,
1219                        self.config.code_distance(),
1220                    )?;
1221                    inventories.push(self.compile_definition_variants(
1222                        &projected,
1223                        &signatures,
1224                        &oriented_sites,
1225                        seam_faces,
1226                        &spatial_ports,
1227                    )?);
1228                }
1229
1230                for inventory in inventories {
1231                    for (position, entries) in inventory {
1232                        let site = &oriented_sites[&position];
1233                        debug_assert_eq!(&site.definition, name);
1234                        debug_assert!(site.instance_path.is_empty());
1235                        for (signature, info) in entries {
1236                            let key = DefinitionVariantKey {
1237                                local_position: site.local_position,
1238                                signature,
1239                                graph_connectivity: info.graph_connectivity,
1240                            };
1241                            if let Some(previous) = variants.insert(key, info) {
1242                                debug_assert_eq!(previous, info);
1243                            }
1244                        }
1245                    }
1246                }
1247            }
1248            Ok(variants)
1249        };
1250
1251        let results = bloq_graph::map_jobs(&missing_names, jobs, |&name| compile_one(name));
1252        let parts_by_name = missing_names
1253            .into_iter()
1254            .zip(results)
1255            .map(|(name, result)| result.map(|parts| (name.clone(), parts)))
1256            .collect::<Result<_, _>>()?;
1257
1258        Ok(build_definition_objects(
1259            &self.cache,
1260            &names,
1261            parts_by_name,
1262            cached,
1263            definition_keys,
1264            &orientations_by_name,
1265            program,
1266        ))
1267    }
1268
1269    fn prepare_proxy(&self, graph: &BlockGraph) -> Result<PreparedProxy, CompileError> {
1270        let ProxyPreflight {
1271            graph,
1272            signatures,
1273            stabilizers,
1274            spatial_ports,
1275        } = self.preflight_proxy(graph)?;
1276        self.report_progress(CompileStage::Templates)?;
1277        let mut compiled = self.compile_blocks(&graph, &ordered_blocks(&graph), &signatures)?;
1278        for expansion in spatial_ports.values() {
1279            let derived = compiled
1280                .get_mut(&expansion.source)
1281                .expect("derived cube was compiled");
1282            derived.graph_connectivity = derived
1283                .graph_connectivity
1284                .with_pipe(expansion.cube_pipe_dir());
1285        }
1286        let spatial_port_templates = self.compile_spatial_port_templates(&spatial_ports)?;
1287        let plan = crate::lower::LinkedPlanInput::from_module_graph(
1288            &graph,
1289            &spatial_ports,
1290            compiled.keys().copied(),
1291        );
1292        let temporal_templates = self.compile_temporal_template_plans(plan.temporal_templates())?;
1293        let wall_templates = self.compile_spatial_template_walls(&graph, plan.spatial_walls())?;
1294        let needs_padding =
1295            !spatial_ports.is_empty() || graph.pipes().any(|pipe| !pipe.dir().is_spatial());
1296        let padding_templates = self
1297            .cache
1298            .padding
1299            .prepare(needs_padding, self.config.code_distance())?;
1300        let template_pool = self.cache.pool_snapshot();
1301        Ok(PreparedProxy {
1302            graph,
1303            stabilizers,
1304            spatial_ports,
1305            compiled,
1306            spatial_port_templates,
1307            plan,
1308            temporal_templates,
1309            wall_templates,
1310            padding_templates,
1311            template_pool,
1312        })
1313    }
1314
1315    fn lower_prepared_proxy(
1316        config: CompileConfig,
1317        prepared: PreparedProxy,
1318    ) -> Result<CompileArtifacts, CompileError> {
1319        let PreparedProxy {
1320            graph,
1321            stabilizers,
1322            spatial_ports,
1323            compiled,
1324            spatial_port_templates,
1325            plan,
1326            temporal_templates,
1327            wall_templates,
1328            padding_templates,
1329            template_pool,
1330        } = prepared;
1331        let plan = crate::lower::LowerPlan::from_linked_input(&plan);
1332        let mut bloq = lower_clifford_proxy(
1333            PhysicalInput {
1334                graph: &graph,
1335                plan: &plan,
1336                compiled: &compiled,
1337                spatial_port_templates: &spatial_port_templates,
1338                spatial_ports: &spatial_ports,
1339                temporal_templates: &temporal_templates,
1340                wall_templates: &wall_templates,
1341                template_pool: &template_pool,
1342                layout: BlockLayout::new(config.code_distance()),
1343            },
1344            &stabilizers,
1345        )?;
1346        bloq.insert_metadata(CODE_DISTANCE_METADATA_KEY, config.code_distance());
1347        bloq.insert_metadata(CONVENTION_METADATA_KEY, "fixed-bulk".to_owned());
1348        // Memory-padding provenance: edge-owned padding templates, so IR-level
1349        // `Bloq::insert_memory_rounds` works on the saved program alone.
1350        crate::padding::record_edge_padding(
1351            &padding_templates,
1352            &mut bloq,
1353            &graph,
1354            config.code_distance(),
1355        )?;
1356        Ok(CompileArtifacts {
1357            bloq,
1358            compile_duration: Duration::ZERO,
1359            warnings: crate::config::spatial_hadamard_distance_warning(&graph)
1360                .into_iter()
1361                .collect(),
1362        })
1363    }
1364
1365    fn compile_definition_variants(
1366        &self,
1367        graph: &BlockGraph,
1368        signatures: &BlockSignatureMap,
1369        module_sites: &std::collections::HashMap<IVec3, bloq_graph::MaterializedModuleSite>,
1370        seam_faces: &std::collections::HashMap<(String, IVec3), Vec<bloq_graph::Direction>>,
1371        spatial_ports: &SpatialPortExpansionMap,
1372    ) -> Result<CompiledDefinitionVariantMap, CompileError> {
1373        let mut inventory = CompiledDefinitionVariantMap::default();
1374        for (&position, &signature) in signatures {
1375            let block = graph
1376                .get_block(position)
1377                .expect("compiled site belongs to the prepared graph");
1378            let mut signature = signature;
1379            let mut exact_graph_connectivity = BlockSignature::graph_connectivity(block, graph);
1380            if let Some(expansion) = spatial_ports.get(&position) {
1381                exact_graph_connectivity =
1382                    exact_graph_connectivity.with_pipe(expansion.cube_pipe_dir());
1383                signature.connectivity =
1384                    signature.connectivity.with_pipe(expansion.cube_pipe_dir());
1385            }
1386            let site = module_sites
1387                .get(&position)
1388                .expect("module materialization records every compiled block");
1389            let mutable_faces = seam_faces
1390                .get(&(site.definition.clone(), site.local_position))
1391                .map(|directions| {
1392                    directions
1393                        .iter()
1394                        .map(|&direction| site.orientation.rotate_direction(direction))
1395                        .collect::<Vec<_>>()
1396                })
1397                .unwrap_or_default();
1398            let mut connectivity = vec![(signature.connectivity, exact_graph_connectivity)];
1399            for direction in &mutable_faces {
1400                let direction = *direction;
1401                let mut next = Vec::with_capacity(connectivity.len() * 2);
1402                for (template, graph) in connectivity {
1403                    next.push((
1404                        if direction.is_spatial() {
1405                            template.with_plain_pipe(direction)
1406                        } else {
1407                            template
1408                        },
1409                        graph.with_plain_pipe(direction),
1410                    ));
1411                    next.push((
1412                        if direction.is_spatial() {
1413                            template.with_hadamard(direction)
1414                        } else {
1415                            template
1416                        },
1417                        graph.with_hadamard(direction),
1418                    ));
1419                }
1420                connectivity = next;
1421            }
1422
1423            let mut candidates = crate::FxSet::default();
1424            for (template_connectivity, graph_connectivity) in connectivity {
1425                let schedules: &[crate::signature::LayerSchedule] =
1426                    if matches!(signature.kind, BlockKind::Cube(_)) {
1427                        match template_connectivity.hadamard_wall_axes() {
1428                            (true, true) => continue,
1429                            (true, false) => &[crate::signature::LayerSchedule::Extended],
1430                            (false, true) => &[crate::signature::LayerSchedule::ExtendedY],
1431                            (false, false) => &[
1432                                crate::signature::LayerSchedule::Compact,
1433                                crate::signature::LayerSchedule::Padded,
1434                                crate::signature::LayerSchedule::Extended,
1435                                crate::signature::LayerSchedule::ExtendedY,
1436                            ],
1437                        }
1438                    } else {
1439                        &[]
1440                    };
1441                if schedules.is_empty() {
1442                    let mut candidate = signature;
1443                    candidate.connectivity = template_connectivity;
1444                    if candidate.kind == BlockKind::T {
1445                        for side in [
1446                            bloq_graph::Direction::YMINUS,
1447                            bloq_graph::Direction::XPLUS,
1448                            bloq_graph::Direction::YPLUS,
1449                            bloq_graph::Direction::XMINUS,
1450                        ] {
1451                            candidate.surgery_side = Some(side);
1452                            candidates.insert((candidate, graph_connectivity));
1453                        }
1454                    } else {
1455                        candidates.insert((candidate, graph_connectivity));
1456                    }
1457                } else {
1458                    for &schedule in schedules {
1459                        let mut candidate = signature;
1460                        candidate.connectivity = template_connectivity;
1461                        candidate.layer_schedule = Some(schedule);
1462                        candidates.insert((candidate, graph_connectivity));
1463                    }
1464                }
1465            }
1466            if matches!(
1467                signature.kind,
1468                BlockKind::Y
1469                    | BlockKind::Measurement(_)
1470                    | BlockKind::Port
1471                    | BlockKind::Selective(_)
1472                    | BlockKind::T
1473            ) && mutable_faces
1474                .iter()
1475                .any(|direction| !direction.is_spatial())
1476            {
1477                candidates = candidates
1478                    .drain()
1479                    .flat_map(|(candidate, graph_connectivity)| {
1480                        [Basis::X, Basis::Z].map(move |basis| {
1481                            (
1482                                BlockSignature {
1483                                    boundary_basis: Some(basis),
1484                                    ..candidate
1485                                },
1486                                graph_connectivity,
1487                            )
1488                        })
1489                    })
1490                    .collect();
1491            }
1492            candidates.insert((signature, exact_graph_connectivity));
1493            let mut variants = Vec::with_capacity(candidates.len());
1494            for (candidate, graph_connectivity) in candidates {
1495                let valid = validate_fixed_bulk(candidate).is_ok();
1496                if !valid && candidate != signature {
1497                    continue;
1498                }
1499                let template = self.compile_block_signature(candidate)?;
1500                variants.push((
1501                    candidate,
1502                    CompiledTemplateInfo {
1503                        graph_connectivity,
1504                        template,
1505                    },
1506                ));
1507            }
1508            inventory.insert(position, variants);
1509        }
1510        Ok(inventory)
1511    }
1512
1513    fn compile_blocks(
1514        &self,
1515        graph: &BlockGraph,
1516        blocks: &[&Block],
1517        signatures: &BlockSignatureMap,
1518    ) -> Result<CompiledTemplateMap, CompileError> {
1519        let mut compiled = crate::FxMap::with_capacity_and_hasher(blocks.len(), Default::default());
1520        let mut proxy_pins = self
1521            .clifford_proxy
1522            .as_ref()
1523            .into_iter()
1524            .flat_map(|proxy| proxy.pins.iter().copied());
1525
1526        for &block in blocks {
1527            let signature = *signatures
1528                .get(&block.pos())
1529                .expect("signature map includes every ordered block");
1530            // Clifford-proxy rewrites are per *site*, not per signature (two
1531            // same-signature selectives may pin to different arms), so they
1532            // select an arm after the signature cache.
1533            let template = if self.clifford_proxy.is_some() {
1534                let pin = signature.kind.is_selective().then(|| {
1535                    proxy_pins
1536                        .next()
1537                        .expect("proxy pin count was validated before compilation")
1538                });
1539                self.proxy_template_ref(signature, pin)?
1540            } else {
1541                self.compile_block_signature(signature)?
1542            };
1543            compiled.insert(
1544                block.pos(),
1545                CompiledTemplateInfo {
1546                    graph_connectivity: BlockSignature::graph_connectivity(block, graph),
1547                    template,
1548                },
1549            );
1550        }
1551
1552        Ok(compiled)
1553    }
1554
1555    fn compile_spatial_port_templates(
1556        &self,
1557        spatial_ports: &SpatialPortExpansionMap,
1558    ) -> Result<CompiledTemplateMap, CompileError> {
1559        let mut compiled = CompiledTemplateMap::default();
1560        for expansion in spatial_ports.values().copied() {
1561            self.check_cancellation()?;
1562            let connectivity = Connectivity::ISOLATED.with_pipe(expansion.port_pipe_dir());
1563            let template = if expansion.role == PortRole::Multiplex {
1564                let output_qubit = expansion
1565                    .output_qubit
1566                    .expect("Multiplex expansions allocate an output qubit");
1567                TemplateRef::Fixed(self.cache.get_or_compile_fixed(
1568                    Signature::MultiplexPort {
1569                        boundary_basis: expansion.boundary_basis(),
1570                        output_qubit,
1571                    },
1572                    || {
1573                        compile_multiplex_port(
1574                            expansion.boundary_basis(),
1575                            self.config.code_distance(),
1576                            output_qubit,
1577                        )
1578                    },
1579                )?)
1580            } else {
1581                self.compile_block_signature(BlockSignature {
1582                    kind: BlockKind::Port,
1583                    rounds: None,
1584                    connectivity,
1585                    boundary_basis: Some(expansion.boundary_basis()),
1586                    layer_schedule: None,
1587                    surgery_side: None,
1588                })?
1589            };
1590            compiled.insert(
1591                expansion.source,
1592                CompiledTemplateInfo {
1593                    graph_connectivity: connectivity,
1594                    template,
1595                },
1596            );
1597        }
1598        Ok(compiled)
1599    }
1600
1601    fn compile_block_signature(
1602        &self,
1603        signature: BlockSignature,
1604    ) -> Result<TemplateRef, CompileError> {
1605        self.check_cancellation()?;
1606        let template = self.cache.get_or_compile(Signature::Block(signature), || {
1607            compile_signature(
1608                signature,
1609                self.config.code_distance(),
1610                self.config.prepare_t_with_mpps(),
1611            )
1612        })?;
1613        self.check_cancellation()?;
1614        Ok(template)
1615    }
1616
1617    /// Rewrite one site's template reference under the Clifford proxy
1618    /// ([`compile_clifford_proxy`]): a selective pins to the arm its next pin
1619    /// bit selects; a T block becomes a perfect MPP input port on its
1620    /// escaped-patch face. Fixed blocks pass through.
1621    fn proxy_template_ref(
1622        &self,
1623        signature: BlockSignature,
1624        pin: Option<bool>,
1625    ) -> Result<TemplateRef, CompileError> {
1626        let proxy = self
1627            .clifford_proxy
1628            .as_ref()
1629            .expect("caller checked the proxy is active");
1630        if signature.kind == BlockKind::T && proxy.purpose == CliffordProxyPurpose::Distance {
1631            return self.compile_block_signature(BlockSignature {
1632                kind: BlockKind::Port,
1633                rounds: None,
1634                connectivity: Connectivity::ISOLATED.with_pipe(bloq_graph::Direction::ZPLUS),
1635                boundary_basis: signature.boundary_basis,
1636                layer_schedule: None,
1637                surgery_side: None,
1638            });
1639        }
1640        let template = self.compile_block_signature(signature)?;
1641        match template {
1642            TemplateRef::Fixed(_) => Ok(template),
1643            TemplateRef::Selective {
1644                when_true,
1645                when_false,
1646            } => Ok(TemplateRef::Fixed(
1647                if pin.expect("selective has a proxy pin") {
1648                    when_true
1649                } else {
1650                    when_false
1651                },
1652            )),
1653            t @ TemplateRef::T { .. } if proxy.purpose == CliffordProxyPurpose::Detslice => Ok(t),
1654            TemplateRef::T { .. } => unreachable!("distance proxies return above"),
1655        }
1656    }
1657
1658    /// Stabilizers of the fixed Clifford program emitted by the proxy: T nodes
1659    /// are ideal input ports and each selective is filled from its site pin.
1660    fn proxy_stabilizers(
1661        &self,
1662        graph: &BlockGraph,
1663        stabilizers: &StabilizerGenerators,
1664    ) -> Result<StabilizerGenerators, CompileError> {
1665        let proxy = self
1666            .clifford_proxy
1667            .as_ref()
1668            .expect("caller checked the proxy is active");
1669        let basis = RuntimeStabilizerBasis::from_generators(stabilizers).with_t_nodes_as_ports();
1670        let mut pins = proxy.pins.iter().copied();
1671        let mut fills = Vec::new();
1672        for block in ordered_blocks(graph) {
1673            let BlockKind::Selective(kind) = block.kind() else {
1674                continue;
1675            };
1676            let pin = pins
1677                .next()
1678                .expect("compile_clifford_proxy validated the selective pin count");
1679            let chosen = if pin {
1680                kind.pauli_if_true()
1681            } else {
1682                kind.pauli_if_false()
1683            };
1684            fills.push((block.pos(), chosen));
1685        }
1686        debug_assert!(pins.next().is_none());
1687
1688        // Resolve the source rows in the selected arms; re-solving the pinned
1689        // graph would replace the observable basis being tested.
1690        Ok(basis.apply_selective_fills(&fills)?.into_generators())
1691    }
1692
1693    fn compile_temporal_template_plans(
1694        &self,
1695        plans: impl IntoIterator<Item = crate::lower::TemplatePlan>,
1696    ) -> Result<crate::FxMap<crate::lower::TemplatePlan, LoweringTemplateId>, CompileError> {
1697        let mut templates = crate::FxMap::default();
1698        for template_plan in plans {
1699            let crate::lower::TemplatePlan { top_basis, .. } = template_plan;
1700            let template = self.temporal_template(top_basis)?;
1701            templates.insert(template_plan, template);
1702        }
1703        Ok(templates)
1704    }
1705
1706    fn compile_spatial_template_walls(
1707        &self,
1708        graph: &BlockGraph,
1709        walls: impl IntoIterator<Item = crate::lower::SpatialPipeRef>,
1710    ) -> Result<crate::FxMap<crate::lower::SpatialPipeRef, LoweringTemplateId>, CompileError> {
1711        let mut templates = crate::FxMap::default();
1712        for wall in walls {
1713            self.check_cancellation()?;
1714            let distance = self.config.code_distance();
1715            let key = spatial_hadamard_key(graph, wall, distance)?;
1716            let template = self
1717                .cache
1718                .get_or_compile_fixed(Signature::SpatialHadamardPipe(key), || {
1719                    compile_spatial_hadamard(distance, key)
1720                })?;
1721            templates.insert(wall, template);
1722        }
1723        Ok(templates)
1724    }
1725
1726    pub(crate) fn temporal_template(
1727        &self,
1728        top_basis: Basis,
1729    ) -> Result<LoweringTemplateId, CompileError> {
1730        self.check_cancellation()?;
1731        let distance = self.config.code_distance();
1732        let template = self
1733            .cache
1734            .get_or_compile_fixed(Signature::Realignment { top_basis }, || {
1735                compile_realignment(distance, top_basis)
1736            })?;
1737        self.check_cancellation()?;
1738        Ok(template)
1739    }
1740}
1741
1742/// Derive a wall's cache key from its two endpoint cubes.
1743///
1744/// The graph validator has already established that the transverse bases flip
1745/// across the pipe, so the wall may take the negative-axis cube's perpendicular
1746/// face basis as the boundary basis for both halves. The endpoint check below is
1747/// defensive because `Walking` signatures omit spatial faces.
1748fn spatial_hadamard_key(
1749    graph: &BlockGraph,
1750    wall: crate::lower::SpatialPipeRef,
1751    distance: u32,
1752) -> Result<SpatialHadamardKey, CompileError> {
1753    let endpoint = |pos: IVec3| -> Result<(&Block, bloq_graph::CubeKind), CompileError> {
1754        let block = graph
1755            .get_endpoint_block(pos)
1756            .ok_or(CompileError::SpatialHadamardUnsupportedEndpoint { pos })?;
1757        match block.kind() {
1758            BlockKind::Cube(kind) => Ok((block, kind)),
1759            _ => Err(CompileError::SpatialHadamardUnsupportedEndpoint { pos }),
1760        }
1761    };
1762    let (minus_block, minus_kind) = endpoint(wall.src)?;
1763    let (plus_block, plus_kind) = endpoint(wall.dst)?;
1764    let axis = wall.axis();
1765    let boundary_basis = match axis {
1766        bloq_graph::UDirection::X => minus_kind.y(),
1767        bloq_graph::UDirection::Y => minus_kind.x(),
1768        bloq_graph::UDirection::Z => unreachable!("spatial Hadamard walls are not temporal"),
1769    };
1770    let make_side = |block: &Block, kind: bloq_graph::CubeKind| {
1771        let connectivity = BlockSignature::template_connectivity(block, graph);
1772        WallSide {
1773            temporal_basis: kind.z(),
1774            connectivity,
1775        }
1776    };
1777    Ok(SpatialHadamardKey {
1778        axis,
1779        // The wall runs one round per cube round. Its two endpoints are cubes
1780        // in one z-layer joined by a spacelike pipe, so they are in the same
1781        // spatial component and the validator has already established that they
1782        // share a height — the `max` is belt and braces, not a real choice.
1783        rounds: crate::signature::cube_rounds(minus_block, distance)?
1784            .max(crate::signature::cube_rounds(plus_block, distance)?),
1785        boundary_basis,
1786        minus: make_side(minus_block, minus_kind),
1787        plus: make_side(plus_block, plus_kind),
1788    })
1789}
1790
1791/// Compile one block signature into its circuit template(s). A selective block
1792/// becomes two per-basis templates (one per resolved Pauli), turned into a
1793/// guarded component by lowering; a T block becomes its cultivation + escape stage
1794/// pair, turned into a `RepeatUntilSuccess` region. Everything else is a single
1795/// fixed template.
1796///
1797/// Free-standing rather than a method because the cache runs it outside its
1798/// lock: it takes only the config values it needs, never the context.
1799fn compile_signature(
1800    signature: BlockSignature,
1801    distance: u32,
1802    prepare_t_with_mpps: bool,
1803) -> Result<crate::cache::CompiledTemplates, CompileError> {
1804    use crate::cache::CompiledTemplates;
1805
1806    if let BlockKind::Selective(kind) = signature.kind {
1807        let boundary_basis = signature
1808            .boundary_basis
1809            .expect("validate ensures a selective block has a boundary basis");
1810        let SelectiveTemplates {
1811            when_true,
1812            when_false,
1813        } = compile_selective(signature.connectivity, distance, boundary_basis, kind)?;
1814        return Ok(CompiledTemplates::Selective {
1815            when_true,
1816            when_false,
1817        });
1818    }
1819    if signature.kind == BlockKind::T {
1820        let boundary_basis = signature
1821            .boundary_basis
1822            .expect("validate ensures a T block has a boundary basis");
1823        let side = signature
1824            .surgery_side
1825            .expect("block_signatures picks a surgery side for every T block");
1826        // `boundary_basis` is the escaped patch's y-face (top) basis, exactly
1827        // as `compile_y` consumes it; the x-face basis is its flip.
1828        let layout = crate::block::fixed_bulk::t::SurgeryLayout::new(
1829            side,
1830            boundary_basis.flip(),
1831            boundary_basis,
1832            distance,
1833        );
1834        if prepare_t_with_mpps {
1835            return Ok(CompiledTemplates::Fixed(std::sync::Arc::new(
1836                crate::block::fixed_bulk::t::prepare_t_with_mpps(&layout)?,
1837            )));
1838        }
1839        let cultivation = crate::block::fixed_bulk::t::steane::cultivation_template(&layout)?;
1840        let escape = crate::block::fixed_bulk::t::escape::escape_template(&layout)?;
1841        return Ok(CompiledTemplates::T {
1842            cultivation: std::sync::Arc::new(cultivation),
1843            escape: std::sync::Arc::new(escape),
1844        });
1845    }
1846    let template = compile_fixed_bulk(signature, distance)?;
1847    Ok(CompiledTemplates::Fixed(template))
1848}
1849
1850fn ordered_blocks(graph: &BlockGraph) -> Vec<&Block> {
1851    let mut blocks: Vec<_> = graph.blocks().collect();
1852    blocks.sort_unstable_by_key(|block| {
1853        let pos = block.pos();
1854        (pos.z, pos.x, pos.y)
1855    });
1856    blocks
1857}
1858
1859/// Clifford proxies pin only selective measurements, so they cannot erase structural
1860/// runtime branches before dropping the action track.
1861fn reject_structural_proxy(graph: &BlockGraph) -> Result<(), CompileError> {
1862    if !graph.branch_regions()?.is_empty() {
1863        return Err(CompileError::StructuralBranchCliffordProxyUnsupported);
1864    }
1865    Ok(())
1866}
1867
1868#[cfg(test)]
1869mod tests {
1870    use bloq_ir::{LevelPath, MemoryRoundTarget};
1871
1872    use super::*;
1873    use crate::signature::LayerSchedule;
1874    use bloq_graph::{
1875        Action, BranchArm, CubeKind, Direction, GalleryItem, MeasureTarget, PatchRotationKind,
1876        Pipe, WalkingBoundaryKind, WalkingKind,
1877    };
1878    use glam::ivec3;
1879    use rstest::rstest;
1880    // Structural checks that mean "how many blocks lowered" use
1881    // `Bloq::quantum_node_count`: observables lower to extra classical nodes,
1882    // so total `node_count()` does not equal the number of blocks.
1883
1884    #[test]
1885    fn compilation_entry_points_honor_resource_limits() {
1886        let graph = GalleryItem::CNOT
1887            .build()
1888            .flatten()
1889            .expect("gallery graph expands");
1890        let module = graph.clone().with_inferred_interface().unwrap();
1891        let config =
1892            CompileConfig::default().with_certification_limits(ModuleCertificationLimits {
1893                max_occupied_cells: 0,
1894                ..ModuleCertificationLimits::DEFAULT
1895            });
1896        let context = CompileContext::new(config);
1897        for error in [
1898            context.compile(&graph).unwrap_err(),
1899            context.compile(&module).unwrap_err(),
1900            compile_clifford_proxy(config, &graph, &[]).unwrap_err(),
1901        ] {
1902            assert!(error.is_resource_limited(), "{error}");
1903            assert!(
1904                error.to_string().contains("occupied footprint cells"),
1905                "{error}"
1906            );
1907        }
1908
1909        let config =
1910            CompileConfig::default().with_certification_limits(ModuleCertificationLimits {
1911                max_boolean_nodes: 0,
1912                ..ModuleCertificationLimits::DEFAULT
1913            });
1914        let error = CompileContext::new(config)
1915            .compile(&two_structural_branch_graph())
1916            .unwrap_err();
1917        assert!(error.to_string().contains("Boolean nodes"), "{error}");
1918        assert!(error.is_resource_limited());
1919
1920        let limits = ModuleCertificationLimits {
1921            max_boolean_steps: 0,
1922            ..ModuleCertificationLimits::DEFAULT
1923        };
1924        let error = CompileContext::new(CompileConfig::default().with_certification_limits(limits))
1925            .compile(&graph)
1926            .unwrap_err();
1927        assert!(error.is_resource_limited(), "{error}");
1928        assert_eq!(
1929            error.resource_limit_help(),
1930            Some(ModuleCertificationLimits::RESOURCE_LIMIT_HELP)
1931        );
1932        assert!(
1933            error.to_string().contains("with_certification_limits"),
1934            "{error}"
1935        );
1936        CompileContext::new(CompileConfig::default())
1937            .compile(&graph)
1938            .unwrap();
1939
1940        let exhausted = bloq_utils::boolean::BooleanResourceError {
1941            resource: "Boolean work steps",
1942            observed: 2,
1943            limit: 1,
1944        };
1945        let parse_error = bloq_graph::ParseError::Graph {
1946            source: Box::new(bloq_graph::BlockGraphError::from(exhausted)),
1947            span: None,
1948        };
1949        for (error, certification_help) in [
1950            (CompileError::from(exhausted), true),
1951            (
1952                CompileError::from(bloq_ir::BloqValidationError::from(exhausted)),
1953                false,
1954            ),
1955            (
1956                CompileError::from(bloq_circuit::CircuitError::FlattenResourceLimit {
1957                    observed: 2,
1958                    limit: 1,
1959                }),
1960                false,
1961            ),
1962            (
1963                CompileError::from(bloq_graph::RuntimeBasisError::from(exhausted)),
1964                true,
1965            ),
1966            (
1967                CompileError::from(bloq_graph::BlockGraphError::Zx(bloq_graph::ZXError::Graph(
1968                    Box::new(bloq_graph::BlockGraphError::from(exhausted)),
1969                ))),
1970                true,
1971            ),
1972            (
1973                CompileError::from(bloq_graph::ModuleError::Parse(parse_error.clone())),
1974                true,
1975            ),
1976            (
1977                CompileError::from(bloq_graph::BlockGraphError::Parse(parse_error)),
1978                true,
1979            ),
1980        ] {
1981            assert!(error.is_resource_limited(), "{error:?}");
1982            assert_eq!(
1983                error.resource_limit_help().is_some(),
1984                certification_help,
1985                "{error:?}"
1986            );
1987        }
1988        assert!(!CompileError::InvalidDistance(2).is_resource_limited());
1989        assert!(
1990            CompileError::InvalidDistance(2)
1991                .resource_limit_help()
1992                .is_none()
1993        );
1994    }
1995
1996    #[test]
1997    fn verification_audit_hints_only_for_inherited_boolean_limits() {
1998        use bloq_ir::lowering::NodeTemplateInstanceMergeError;
1999        use bloq_ir::{BloqNodeId, BloqValidationError, TemplateId};
2000
2001        let exhausted = bloq_utils::boolean::BooleanResourceError {
2002            resource: "Boolean work steps",
2003            observed: 2,
2004            limit: 1,
2005        };
2006        for error in [
2007            BloqValidationError::BooleanResource(exhausted),
2008            BloqValidationError::InvalidInstanceMergeStructure {
2009                node: BloqNodeId(3),
2010                source: NodeTemplateInstanceMergeError::BooleanResource(exhausted),
2011            },
2012            BloqValidationError::InvalidTemplateCircuit {
2013                template: TemplateId(4),
2014                source: NodeTemplateInstanceMergeError::BooleanResource(exhausted),
2015            },
2016        ] {
2017            let error = CompileError::from_verification_audit(error);
2018            assert!(
2019                matches!(&error, CompileError::BooleanResource(resource) if *resource == exhausted)
2020            );
2021            assert_eq!(
2022                error.resource_limit_help(),
2023                Some(ModuleCertificationLimits::RESOURCE_LIMIT_HELP)
2024            );
2025        }
2026        for (error, resource_limited) in [
2027            (
2028                BloqValidationError::InvalidInstanceMergeStructure {
2029                    node: BloqNodeId(3),
2030                    source: NodeTemplateInstanceMergeError::BodyCountOverflow,
2031                },
2032                true,
2033            ),
2034            (
2035                BloqValidationError::InvalidTemplateCircuit {
2036                    template: TemplateId(4),
2037                    source: NodeTemplateInstanceMergeError::NoiseRepeatExpansion(
2038                        bloq_ir::FlattenError::Circuit(
2039                            bloq_circuit::CircuitError::FlattenResourceLimit {
2040                                observed: 2,
2041                                limit: 1,
2042                            },
2043                        ),
2044                    ),
2045                },
2046                true,
2047            ),
2048            (BloqValidationError::CyclicGraph, false),
2049        ] {
2050            let converted = CompileError::from_verification_audit(error.clone());
2051            assert!(matches!(&converted, CompileError::BloqValidation(source) if source == &error));
2052            assert_eq!(converted.is_resource_limited(), resource_limited);
2053            assert!(converted.resource_limit_help().is_none());
2054        }
2055    }
2056
2057    #[test]
2058    fn clifford_proxy_preserves_source_generator_selection_and_order() {
2059        let graph = GalleryItem::CNOT
2060            .build()
2061            .flatten()
2062            .expect("gallery graph expands");
2063        let mut source = graph.stabilizers().unwrap();
2064        source.generators.reverse();
2065        source.generators.pop().expect("CNOT has generators");
2066        let ctx = CompileContext {
2067            clifford_proxy: Some(CliffordProxyPins {
2068                pins: Vec::new(),
2069                purpose: CliffordProxyPurpose::Distance,
2070                source_summary: None,
2071            }),
2072            ..CompileContext::new(CompileConfig::default())
2073        };
2074        let resolved = ctx.proxy_stabilizers(&graph, &source).unwrap();
2075        assert_eq!(resolved.generators, source.generators);
2076    }
2077
2078    fn single_cube_runtime_graph() -> BlockGraph {
2079        let mut graph = BlockGraph::new();
2080        graph.add_block(bloq_graph::Block::new(
2081            ivec3(0, 0, 0),
2082            BlockKind::Cube(CubeKind::ZXZ),
2083        ));
2084        graph
2085            .add_action(Action::Measure {
2086                target: MeasureTarget::Node(ivec3(0, 0, 0)),
2087                name: "skip-derive".to_string(),
2088            })
2089            .expect("add skip-derive measurement action");
2090        graph
2091    }
2092
2093    fn structural_branch_graph() -> BlockGraph {
2094        let prefix = ivec3(0, 0, 0);
2095        let target = ivec3(0, 0, 1);
2096        let reader = ivec3(2, 0, 0);
2097        let mut graph = BlockGraph::new();
2098        for pos in [prefix, reader] {
2099            graph.add_block(Block::new(pos, BlockKind::Cube(CubeKind::ZXZ)));
2100        }
2101        let false_arm = BranchArm::new(
2102            vec![Block::new(target, BlockKind::Measurement(Basis::Z))],
2103            vec![Pipe::new(prefix, Direction::ZPLUS).with_hadamard()],
2104        );
2105        let true_arm = BranchArm::new(
2106            vec![Block::new(target, BlockKind::Cube(CubeKind::XZX))],
2107            vec![Pipe::new(prefix, Direction::ZPLUS).with_hadamard()],
2108        );
2109        let target = graph
2110            .try_add_branch_region("b0", false_arm, true_arm)
2111            .unwrap();
2112        graph
2113            .set_actions(vec![
2114                Action::Measure {
2115                    target: MeasureTarget::Node(reader),
2116                    name: "m".into(),
2117                },
2118                Action::Branch {
2119                    target,
2120                    condition: bloq_graph::Expr::Var("m".into()),
2121                },
2122            ])
2123            .unwrap();
2124        graph
2125    }
2126
2127    fn mixed_structural_dynamic_graph() -> BlockGraph {
2128        let mut graph = GalleryItem::T
2129            .build()
2130            .flatten()
2131            .expect("gallery graph expands");
2132        let prefix = ivec3(4, 0, 0);
2133        let target = prefix + IVec3::Z;
2134        graph.add_block(Block::new(prefix, BlockKind::Cube(CubeKind::ZXZ)));
2135        let false_arm = BranchArm::new(
2136            vec![Block::new(target, BlockKind::Measurement(Basis::Z))],
2137            vec![Pipe::new(prefix, Direction::ZPLUS).with_hadamard()],
2138        );
2139        let true_arm = BranchArm::new(
2140            vec![Block::new(target, BlockKind::Cube(CubeKind::XZX))],
2141            vec![Pipe::new(prefix, Direction::ZPLUS).with_hadamard()],
2142        );
2143        let target = graph
2144            .try_add_branch_region("b0", false_arm, true_arm)
2145            .unwrap();
2146        let mut actions = graph.actions();
2147        actions.push(Action::Branch {
2148            target,
2149            condition: bloq_graph::Expr::Var("mzz".into()),
2150        });
2151        graph.set_actions(actions).unwrap();
2152        graph
2153    }
2154
2155    fn two_structural_branch_graph() -> BlockGraph {
2156        let mut graph = GalleryItem::GHZ
2157            .build()
2158            .flatten()
2159            .expect("gallery graph expands");
2160        let mut actions = Vec::new();
2161        for (index, (past, kind)) in [
2162            (ivec3(0, -1, 1), CubeKind::XZZ),
2163            (ivec3(-1, 0, 1), CubeKind::ZXZ),
2164        ]
2165        .into_iter()
2166        .enumerate()
2167        {
2168            let target = past + IVec3::Z;
2169            let reader = ivec3(20 + index as i32, 0, 0);
2170            graph.set_block_kind(past, BlockKind::Cube(kind)).unwrap();
2171            graph.add_block(Block::new(reader - IVec3::Z, BlockKind::Port));
2172            graph.add_block(Block::new(reader, BlockKind::Cube(CubeKind::XZX)));
2173            graph.add_pipe(Pipe::new(reader - IVec3::Z, Direction::ZPLUS));
2174            let arm = |kind| {
2175                BranchArm::new(
2176                    vec![Block::new(target, kind)],
2177                    vec![Pipe::new(past, Direction::ZPLUS)],
2178                )
2179            };
2180            let target = graph
2181                .try_add_branch_region(
2182                    format!("b{index}"),
2183                    arm(BlockKind::Measurement(Basis::Z)),
2184                    arm(BlockKind::Cube(kind)),
2185                )
2186                .unwrap();
2187            let measurement = format!("m{index}");
2188            actions.push(Action::Measure {
2189                target: MeasureTarget::Node(reader),
2190                name: measurement.clone(),
2191            });
2192            actions.push(Action::Branch {
2193                target,
2194                condition: bloq_graph::Expr::Var(measurement),
2195            });
2196        }
2197        graph.set_actions(actions).unwrap();
2198        graph
2199    }
2200
2201    #[test]
2202    fn audited_structural_branch_retains_compiled_artifacts() {
2203        let graph = structural_branch_graph();
2204        let context = CompileContext::new(CompileConfig::new(3));
2205
2206        let artifacts = context.compile_and_validate(&graph).unwrap();
2207        assert_eq!(compiled_distance(&artifacts.bloq), Some(3));
2208        assert!(artifacts.bloq.quantum_node_count() > 0);
2209        assert!(artifacts.bloq.has_conditional_membership());
2210    }
2211
2212    #[test]
2213    fn repeated_branch_variables_obey_classical_slot_exactness() {
2214        for bit in [false, true] {
2215            for operation in [
2216                bloq_graph::BinaryOp::Xor,
2217                bloq_graph::BinaryOp::And,
2218                bloq_graph::BinaryOp::Or,
2219            ] {
2220                let mut graph = structural_branch_graph();
2221                let mut actions = graph.actions();
2222                let Action::Branch { condition, .. } = actions.last_mut().unwrap() else {
2223                    unreachable!()
2224                };
2225                let leaf = if bit {
2226                    bloq_graph::Expr::Not(Box::new(condition.clone()))
2227                } else {
2228                    condition.clone()
2229                };
2230                *condition =
2231                    bloq_graph::Expr::Binary(operation, Box::new(leaf.clone()), Box::new(leaf));
2232                graph.set_actions(actions).unwrap();
2233                let program = compile(&graph, 3).unwrap();
2234                program.validate().unwrap();
2235                let report = bloq_vm::run_bloq(&program, 2, 0).unwrap();
2236                let expected = operation != bloq_graph::BinaryOp::Xor && bit;
2237                assert_eq!(report.branch_selectors, [expected; 2]);
2238                assert_eq!(report.discarded, 0);
2239                assert!(report.all_detectors_constant());
2240            }
2241        }
2242    }
2243
2244    #[test]
2245    fn structural_branch_composes_with_t_and_selective_components() {
2246        use bloq_ir::{BloqNodeKind, RegionNode};
2247
2248        let graph = mixed_structural_dynamic_graph();
2249        let context = CompileContext::new(CompileConfig::new(3));
2250
2251        let program = context.compile_and_validate(&graph).unwrap().bloq;
2252        let regions = program
2253            .nodes()
2254            .filter(|(_, node)| {
2255                matches!(
2256                    node.kind,
2257                    BloqNodeKind::Region(RegionNode::RepeatUntilSuccess { .. })
2258                )
2259            })
2260            .count();
2261        assert_eq!(regions, 1);
2262        assert!(program.has_conditional_membership());
2263        assert!(program.nodes().any(|(_, node)| matches!(
2264            node.provenance,
2265            bloq_ir::NodeProvenance::BranchSelector { .. }
2266        )));
2267
2268        let report = bloq_vm::run_bloq(&program, 8, 7).unwrap();
2269        assert_eq!(report.discarded, 0);
2270        assert!(report.all_detectors_constant());
2271        assert!(report.max_rank > 1);
2272        assert_eq!(report.frame_pairs.len(), 1);
2273        assert!(report.branch_selectors.contains(&false));
2274        assert!(report.branch_selectors.contains(&true));
2275    }
2276
2277    #[test]
2278    fn compile_and_validate_checks_false_arm_physical_signatures() {
2279        let mut graph = BlockGraph::new();
2280        let prefix = ivec3(0, 0, 0);
2281        let a = ivec3(0, 0, 1);
2282        let b = ivec3(1, 0, 1);
2283        let c = ivec3(1, 1, 1);
2284        let reader = ivec3(4, 0, 0);
2285        graph.add_block(Block::new(prefix, BlockKind::Cube(CubeKind::XZX)));
2286        graph.add_block(Block::new(reader, BlockKind::Cube(CubeKind::ZXZ)));
2287        let target = graph
2288            .try_add_branch_region(
2289                "mixed_wall",
2290                BranchArm::new(
2291                    vec![
2292                        Block::new(a, BlockKind::Cube(CubeKind::XZX)),
2293                        Block::new(b, BlockKind::Cube(CubeKind::XXZ)),
2294                        Block::new(c, BlockKind::Cube(CubeKind::ZXX)),
2295                    ],
2296                    vec![
2297                        Pipe::new(prefix, Direction::ZPLUS),
2298                        Pipe::new(a, Direction::XPLUS).with_hadamard(),
2299                        Pipe::new(b, Direction::YPLUS).with_hadamard(),
2300                    ],
2301                ),
2302                BranchArm::new(
2303                    vec![Block::new(a, BlockKind::Cube(CubeKind::XZX))],
2304                    vec![Pipe::new(prefix, Direction::ZPLUS)],
2305                ),
2306            )
2307            .unwrap();
2308        graph
2309            .set_actions(vec![
2310                Action::Measure {
2311                    target: MeasureTarget::Node(reader),
2312                    name: "m".into(),
2313                },
2314                Action::Branch {
2315                    target,
2316                    condition: bloq_graph::Expr::Var("m".into()),
2317                },
2318            ])
2319            .unwrap();
2320        let context = CompileContext::new(CompileConfig::new(3));
2321        assert!(matches!(
2322            context.compile_and_validate(&graph),
2323            Err(CompileError::MixedSpatialHadamardUnsupported { .. })
2324        ));
2325        assert!(matches!(
2326            context.compile(&graph),
2327            Err(CompileError::MixedSpatialHadamardUnsupported { .. })
2328        ));
2329    }
2330
2331    #[test]
2332    fn t_spill_reserves_both_structural_arms() {
2333        let mut graph = BlockGraph::new();
2334        let t = ivec3(0, 0, 1);
2335        let prefix = ivec3(1, -1, 0);
2336        let arm = ivec3(1, -1, 1);
2337        let spill = ivec3(0, -1, 1);
2338        let reader = ivec3(4, 0, 0);
2339        graph.add_block(Block::new(t, BlockKind::T));
2340        graph.add_block(Block::new([0, 0, 2], BlockKind::Port));
2341        graph.add_pipe(Pipe::new(t, Direction::ZPLUS));
2342        for position in [prefix, reader] {
2343            graph.add_block(Block::new(position, BlockKind::Cube(CubeKind::ZXZ)));
2344        }
2345        let target = graph
2346            .try_add_branch_region(
2347                "spill",
2348                BranchArm::new(
2349                    vec![
2350                        Block::new(arm, BlockKind::Cube(CubeKind::ZXZ)),
2351                        Block::new(spill, BlockKind::Cube(CubeKind::ZXZ)),
2352                    ],
2353                    vec![
2354                        Pipe::new(prefix, Direction::ZPLUS),
2355                        Pipe::new(arm, Direction::XMINUS),
2356                    ],
2357                ),
2358                BranchArm::new(
2359                    vec![Block::new(arm, BlockKind::Cube(CubeKind::ZXZ))],
2360                    vec![Pipe::new(prefix, Direction::ZPLUS)],
2361                ),
2362            )
2363            .unwrap();
2364        for selected in [false, true] {
2365            let condition = bloq_graph::Expr::Var("m".into());
2366            graph
2367                .set_actions(vec![
2368                    Action::Measure {
2369                        target: MeasureTarget::Node(reader),
2370                        name: "m".into(),
2371                    },
2372                    Action::Branch {
2373                        target,
2374                        condition: if selected {
2375                            bloq_graph::Expr::Not(Box::new(condition))
2376                        } else {
2377                            condition
2378                        },
2379                    },
2380                ])
2381                .unwrap();
2382            let context = CompileContext::new(CompileConfig::new(3));
2383            for module in [false, true] {
2384                let program = if module {
2385                    context.compile(&graph.clone().with_inferred_interface().unwrap())
2386                } else {
2387                    context.compile_and_validate(&graph)
2388                }
2389                .unwrap()
2390                .bloq;
2391                crate::validate_bloq_qubit_layout_for_source(&program, &graph).unwrap();
2392                let report = bloq_vm::run_bloq(&program, 2, 7).unwrap();
2393                assert_eq!(report.discarded, 0);
2394                assert!(
2395                    report
2396                        .detectors
2397                        .iter()
2398                        .all(|detector| detector.per_shot.iter().all(|&bit| !bit))
2399                );
2400                assert_eq!(report.branch_selectors, [selected; 2]);
2401            }
2402        }
2403        for position in [ivec3(1, 0, 1), ivec3(0, 1, 1), ivec3(-1, 0, 1)] {
2404            graph.add_block(Block::new(position, BlockKind::Cube(CubeKind::ZXZ)));
2405        }
2406        let context = CompileContext::new(CompileConfig::new(3));
2407        assert!(matches!(
2408            context.compile_and_validate(&graph),
2409            Err(CompileError::TBlockNeedsFreeNeighbor { .. })
2410        ));
2411        assert!(matches!(
2412            context.compile(&graph.with_inferred_interface().unwrap()),
2413            Err(CompileError::TBlockNeedsFreeNeighbor { .. })
2414        ));
2415    }
2416
2417    #[test]
2418    fn independent_structural_deltas_execute_all_joint_choices() {
2419        let module = two_structural_branch_graph()
2420            .with_inferred_interface()
2421            .unwrap();
2422        let context = CompileContext::new(CompileConfig::new(3));
2423        let program = context.compile(&module).unwrap().bloq;
2424        program.validate().unwrap();
2425
2426        let report = bloq_vm::run_bloq(&program, 64, 0xB12A).unwrap();
2427        assert_eq!(report.discarded, 0);
2428        assert!(report.all_detectors_constant());
2429        let choices = report
2430            .branch_selectors
2431            .as_chunks::<2>()
2432            .0
2433            .iter()
2434            .map(|bits| (bits[0], bits[1]))
2435            .collect::<std::collections::HashSet<_>>();
2436        assert_eq!(choices.len(), 4);
2437        assert_eq!(report.frame_pairs.len(), 2);
2438    }
2439
2440    #[test]
2441    fn clifford_proxy_entrypoints_reject_structural_branch() {
2442        let graph = structural_branch_graph();
2443        let rejected = |result: Result<CompileArtifacts, CompileError>| {
2444            assert!(matches!(
2445                result,
2446                Err(CompileError::StructuralBranchCliffordProxyUnsupported)
2447            ));
2448        };
2449
2450        rejected(compile_clifford_proxy(
2451            CompileConfig::default(),
2452            &graph,
2453            &[],
2454        ));
2455        rejected(compile_random_clifford_proxy(
2456            CompileConfig::default(),
2457            &graph,
2458            7,
2459        ));
2460        rejected(compile_detslice_proxy(
2461            CompileConfig::default(),
2462            &graph,
2463            &[],
2464        ));
2465    }
2466
2467    fn source_timeline(program: &Bloq, pos: IVec3) -> Option<&bloq_ir::QuantumTimeline> {
2468        program.nodes().find_map(|(_, node)| {
2469            node.block_members()
2470                .iter()
2471                .any(|member| member.pos == pos)
2472                .then(|| node.expect_quantum().timeline.as_ref())
2473                .flatten()
2474        })
2475    }
2476
2477    #[test]
2478    fn compilation_rejects_measurement_surface_on_output_port() {
2479        let graph = BlockGraph::from_blog_text(
2480            "BLOG 1.0\n\n\
2481             1: XZX [0,0,1]\n\
2482             2: Port [0,0,2]\n\
2483             3: T [1,0,0]\n\
2484             4: XZX [1,0,1]\n\
2485             5: YX [1,0,2]\n\
2486             [0,0,1] -> +Z\n\
2487             [0,0,1] -> +X\n\
2488             [1,0,0] -> +Z\n\
2489             [1,0,2] -> -Z\n\n\
2490             mzz = measure 1 -> +X\n\
2491             resolve 5 if mzz\n",
2492        )
2493        .unwrap();
2494        let stabilizers = graph.stabilizers().unwrap();
2495        let measurement = stabilizers
2496            .generators
2497            .iter()
2498            .find(|row| row.measurement_name() == Some("mzz"))
2499            .expect("the merge supplies the named measurement");
2500        assert_eq!(
2501            measurement.stabilizer.port_stabilizer.get(&ivec3(0, 0, 2)),
2502            Some(&bloq_graph::Pauli::Z),
2503        );
2504        assert!(matches!(
2505            stabilizers.validate_measurements_close_before_outputs(),
2506            Err(bloq_graph::RuntimeBasisError::Stabilizer(
2507                bloq_graph::StabilizerError::MeasurementSurfaceTouchesOutputPort {
2508                    ref name,
2509                    port,
2510                }
2511            )) if name == "mzz" && port == ivec3(0, 0, 2)
2512        ));
2513        assert!(matches!(
2514            compile_clifford_proxy(CompileConfig::default(), &graph, &[false]),
2515            Err(CompileError::RuntimeBasis(
2516                bloq_graph::RuntimeBasisError::Stabilizer(
2517                    bloq_graph::StabilizerError::MeasurementSurfaceTouchesOutputPort {
2518                        ref name,
2519                        port,
2520                    }
2521                )
2522            )) if name == "mzz" && port == ivec3(0, 0, 2)
2523        ));
2524        let context = CompileContext::new(CompileConfig::default());
2525
2526        for error in [
2527            context.compile_and_validate(&graph).unwrap_err(),
2528            context.compile(&graph).unwrap_err(),
2529        ] {
2530            assert!(matches!(
2531                error,
2532                CompileError::BlockGraph(bloq_graph::BlockGraphError::Stabilizer(
2533                    bloq_graph::StabilizerError::UnavailableControlParity { ref mvar, .. }
2534                )) if mvar == "mzz"
2535            ));
2536        }
2537    }
2538
2539    #[test]
2540    fn t_region_observable_ids_fail_before_wrapping() {
2541        let context = CompileContext {
2542            clifford_proxy: Some(CliffordProxyPins {
2543                pins: vec![false],
2544                purpose: CliffordProxyPurpose::Detslice,
2545                source_summary: None,
2546            }),
2547            ..CompileContext::new(CompileConfig::default())
2548        };
2549        let prepared = context
2550            .prepare_proxy(
2551                &GalleryItem::T
2552                    .build()
2553                    .flatten()
2554                    .expect("gallery graph expands"),
2555            )
2556            .unwrap();
2557        let plan = crate::lower::LowerPlan::from_linked_input(&prepared.plan);
2558        let placed = crate::lower::PlacedPlan::new(PhysicalInput {
2559            graph: &prepared.graph,
2560            plan: &plan,
2561            compiled: &prepared.compiled,
2562            spatial_port_templates: &prepared.spatial_port_templates,
2563            spatial_ports: &prepared.spatial_ports,
2564            temporal_templates: &prepared.temporal_templates,
2565            wall_templates: &prepared.wall_templates,
2566            template_pool: &prepared.template_pool,
2567            layout: BlockLayout::new(3),
2568        })
2569        .unwrap();
2570        assert!(matches!(
2571            placed.emit(u32::MAX - 1),
2572            Err(CompileError::BooleanResource(error)) if error.resource == "observable IDs"
2573        ));
2574    }
2575
2576    #[test]
2577    fn compile_and_validate_normalizes_a_small_span_starting_at_i32_min() {
2578        let port = ivec3(0, 0, i32::MIN);
2579        let cube = ivec3(0, 0, i32::MIN + 1);
2580        let mut graph = BlockGraph::new();
2581        graph.add_block(Block::new(port, BlockKind::Port));
2582        graph.add_block(Block::new(cube, BlockKind::Cube(CubeKind::ZXZ)));
2583        graph.add_pipe(bloq_graph::Pipe::new(port, bloq_graph::Direction::ZPLUS));
2584        let ctx = CompileContext::new(CompileConfig::new(3));
2585
2586        ctx.compile_and_validate(&graph)
2587            .expect("the normalized z span is 0..=1");
2588    }
2589
2590    #[test]
2591    fn compile_propagates_block_layout_overflow() {
2592        assert!(matches!(
2593            block_xy_offset(IVec2::MAX, u32::MAX),
2594            Err(CompileError::BlockLayoutCoordinateOverflow { .. })
2595        ));
2596
2597        let graph = single_cube_runtime_graph()
2598            .shift_positions(ivec3(300_000_000, 0, 0))
2599            .expect("source coordinate fits");
2600        let stages = Arc::new(std::sync::Mutex::new(Vec::new()));
2601        let observed = Arc::clone(&stages);
2602        let ctx = CompileContext::new(CompileConfig::new(3))
2603            .with_progress_observer(move |stage| observed.lock().unwrap().push(stage));
2604
2605        assert!(matches!(
2606            ctx.compile(&graph),
2607            Err(CompileError::BlockLayoutCoordinateOverflow {
2608                block_xy,
2609                distance: 3,
2610            }) if block_xy == glam::ivec2(300_000_000, 0)
2611        ));
2612        assert!(!stages.lock().unwrap().contains(&CompileStage::Complete));
2613    }
2614
2615    #[test]
2616    fn boundary_observable_compile_returns_layout_overflow_without_panicking() {
2617        let graph = single_cube_runtime_graph()
2618            .shift_positions(ivec3(i32::MAX, 0, 0))
2619            .expect("boundary source coordinate fits");
2620        let ctx = CompileContext::new(CompileConfig::new(3));
2621
2622        assert!(matches!(
2623            ctx.compile(&graph),
2624            Err(CompileError::BlockLayoutCoordinateOverflow {
2625                block_xy,
2626                distance: 3,
2627            }) if block_xy == glam::ivec2(i32::MAX, 0)
2628        ));
2629    }
2630
2631    #[test]
2632    fn compile_context_reuses_templates_across_compile_calls() {
2633        let first_graph = single_cube_runtime_graph();
2634        let second_graph = single_cube_runtime_graph();
2635        let ctx = CompileContext::new(CompileConfig::new(3));
2636
2637        let first = ctx.compile(&first_graph).expect("compile first graph");
2638        let second = ctx.compile(&second_graph).expect("compile second graph");
2639
2640        assert_eq!(
2641            ctx.cache.pool_len(),
2642            1,
2643            "matching block signatures should reuse a cached template across compiles"
2644        );
2645        assert!(
2646            std::ptr::eq(
2647                first.bloq.templates().iter().next().unwrap().1,
2648                second.bloq.templates().iter().next().unwrap().1,
2649            ),
2650            "emitted programs retain the immutable cached template"
2651        );
2652    }
2653
2654    #[test]
2655    fn cancellation_stops_an_active_stage_and_the_next_request_can_use_the_cache() {
2656        let graph = GalleryItem::CNOT.build();
2657        let cache = crate::SharedCompileCache::new();
2658        for _ in 0..2 {
2659            let token = bloq_graph::CancellationToken::new();
2660            let cancel = token.clone();
2661            let context = CompileContext::with_shared_cache(CompileConfig::default(), &cache)
2662                .with_cancellation(token)
2663                .with_progress_observer(move |stage| {
2664                    if stage == CompileStage::Readouts {
2665                        cancel.cancel();
2666                    }
2667                });
2668            let error = context.compile(&graph).unwrap_err();
2669            assert!(error.is_cancelled());
2670            assert!(!error.is_resource_limited());
2671        }
2672        let fresh = CompileContext::with_shared_cache(CompileConfig::default(), &cache)
2673            .with_cancellation(bloq_graph::CancellationToken::new());
2674        fresh.compile(&graph).unwrap();
2675    }
2676
2677    #[test]
2678    fn cancelled_module_request_does_not_publish_a_partial_cached_object() {
2679        let program = GalleryItem::CNOT.build().with_inferred_interface().unwrap();
2680        let cache = crate::SharedCompileCache::new();
2681        let token = bloq_graph::CancellationToken::new();
2682        let cancel = token.clone();
2683        let context = CompileContext::with_shared_cache(CompileConfig::default(), &cache)
2684            .with_cancellation(token)
2685            .with_progress_observer(move |stage| {
2686                if stage == CompileStage::Readouts {
2687                    cancel.cancel();
2688                }
2689            });
2690        assert!(context.compile_object(&program).unwrap_err().is_cancelled());
2691        let stages = Arc::new(std::sync::Mutex::new(Vec::new()));
2692        let observed = Arc::clone(&stages);
2693        let fresh = CompileContext::with_shared_cache(CompileConfig::default(), &cache)
2694            .with_progress_observer(move |stage| observed.lock().unwrap().push(stage));
2695        fresh.compile_object(&program).unwrap();
2696        assert!(
2697            stages
2698                .lock()
2699                .unwrap()
2700                .contains(&CompileStage::Certification)
2701        );
2702        assert!(!stages.lock().unwrap().contains(&CompileStage::CacheReuse));
2703    }
2704
2705    #[test]
2706    fn progress_observer_reports_native_stages_and_completion() {
2707        let stages = Arc::new(std::sync::Mutex::new(Vec::new()));
2708        let observed = Arc::clone(&stages);
2709        let context = CompileContext::new(CompileConfig::default())
2710            .with_progress_observer(move |stage| observed.lock().unwrap().push(stage));
2711        context
2712            .compile(
2713                &GalleryItem::CNOT
2714                    .build()
2715                    .flatten()
2716                    .expect("gallery graph expands"),
2717            )
2718            .unwrap();
2719        assert_eq!(
2720            *stages.lock().unwrap(),
2721            [
2722                CompileStage::Validation,
2723                CompileStage::Correlations,
2724                CompileStage::Placement,
2725                CompileStage::Templates,
2726                CompileStage::Readouts,
2727                CompileStage::Optimization,
2728                CompileStage::Complete,
2729            ]
2730        );
2731    }
2732
2733    #[test]
2734    fn progress_observer_reports_module_cache_reuse() {
2735        let module = single_cube_runtime_graph()
2736            .with_inferred_interface()
2737            .unwrap();
2738        let stages = Arc::new(std::sync::Mutex::new(Vec::new()));
2739        let observed = Arc::clone(&stages);
2740        let context = CompileContext::new(CompileConfig::default())
2741            .with_progress_observer(move |stage| observed.lock().unwrap().push(stage));
2742        context.compile(&module).unwrap();
2743        assert_eq!(
2744            stages
2745                .lock()
2746                .unwrap()
2747                .iter()
2748                .filter(|&&stage| stage == CompileStage::Complete)
2749                .count(),
2750            1
2751        );
2752        stages.lock().unwrap().clear();
2753        context.compile(&module).unwrap();
2754        assert_eq!(
2755            *stages.lock().unwrap(),
2756            [
2757                CompileStage::Validation,
2758                CompileStage::CacheReuse,
2759                CompileStage::Complete,
2760            ]
2761        );
2762    }
2763
2764    #[test]
2765    fn fixed_measurement_lowers_without_a_region_and_emits_stim() {
2766        let mut graph = BlockGraph::new();
2767        graph.add_block(Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ)));
2768        graph.add_block(Block::new(IVec3::Z, BlockKind::Measurement(Basis::X)));
2769        graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZPLUS));
2770
2771        let program = CompileContext::new(CompileConfig::new(3))
2772            .compile(&graph)
2773            .expect("fixed measurement graph compiles")
2774            .bloq;
2775
2776        program.validate().expect("compiled Bloq validates");
2777        assert!(program.nodes().all(|(_, node)| node.try_region().is_none()));
2778        let stim = bloq_stim::emit_bloq_stim(&program).expect("fixed measurement emits Stim");
2779        assert!(
2780            stim.contains("MX "),
2781            "expected transversal X readout:\n{stim}"
2782        );
2783    }
2784
2785    /// Two ZXZ cubes stacked along z, joined by one temporal pipe (a two-round
2786    /// memory). `pipe` fixes the temporal pipe's authored orientation.
2787    fn stacked_memory_graph(pipe: bloq_graph::Pipe) -> BlockGraph {
2788        let mut graph = BlockGraph::new();
2789        graph.add_block(Block::new(ivec3(0, 0, 0), BlockKind::Cube(CubeKind::ZXZ)));
2790        graph.add_block(Block::new(ivec3(0, 0, 1), BlockKind::Cube(CubeKind::ZXZ)));
2791        graph.add_pipe(pipe);
2792        graph
2793    }
2794
2795    /// CP-04: a temporal pipe authored `+Z` from the lower cube and one authored
2796    /// `−Z` from the upper cube describe the same seam. Canonical pipe
2797    /// orientation must erase that authoring difference, so both compile to a
2798    /// byte-identical artifact.
2799    #[test]
2800    fn reversed_temporal_pipe_orientation_compiles_identically() {
2801        let forward = stacked_memory_graph(bloq_graph::Pipe::new(
2802            ivec3(0, 0, 0),
2803            bloq_graph::Direction::ZPLUS,
2804        ));
2805        let reversed = stacked_memory_graph(bloq_graph::Pipe::new(
2806            ivec3(0, 0, 1),
2807            bloq_graph::Direction::ZMINUS,
2808        ));
2809
2810        let compile = |graph: &BlockGraph| {
2811            CompileContext::new(CompileConfig::new(3))
2812                .compile(graph)
2813                .expect("orientation compiles")
2814                .bloq
2815                .to_binary()
2816        };
2817        let forward_bytes = compile(&forward);
2818        let reversed_bytes = compile(&reversed);
2819
2820        assert_eq!(
2821            forward_bytes, reversed_bytes,
2822            "reversed temporal-pipe authoring must not change the compiled artifact"
2823        );
2824    }
2825
2826    #[test]
2827    fn basisless_temporal_hadamard_compiles_in_both_orientations() {
2828        let compile = |pipe| {
2829            let mut graph = BlockGraph::new();
2830            graph.add_block(Block::new(IVec3::ZERO, BlockKind::Port));
2831            graph.add_block(Block::new(IVec3::Z, BlockKind::Port));
2832            graph.add_pipe(pipe);
2833
2834            let bloq = super::compile(&graph, 3).expect("temporal Hadamard compiles");
2835            bloq.validate().expect("compiled Bloq validates");
2836            bloq.to_binary()
2837        };
2838        let forward = compile(Pipe::new(IVec3::ZERO, Direction::ZPLUS).with_hadamard());
2839        let reversed = compile(Pipe::new(IVec3::Z, Direction::ZMINUS).with_hadamard());
2840
2841        assert_eq!(forward, reversed);
2842    }
2843
2844    #[test]
2845    fn clifford_proxy_derives_exact_output_frames() {
2846        for (gallery, pins) in [(GalleryItem::CNOT, &[][..]), (GalleryItem::T, &[false][..])] {
2847            let bloq = compile_clifford_proxy(
2848                CompileConfig::default(),
2849                &gallery.build().flatten().expect("gallery graph expands"),
2850                pins,
2851            )
2852            .unwrap_or_else(|error| panic!("{gallery:?} proxy compiles: {error}"))
2853            .bloq;
2854            assert_eq!(
2855                bloq.output_frames().len(),
2856                bloq.logical_outputs().len(),
2857                "{gallery:?} proxy emits one frame pair per logical output",
2858            );
2859            bloq.validate()
2860                .expect("proxy ports and recipe owners validate");
2861            let flips = bloq
2862                .edges()
2863                .filter(|edge| {
2864                    matches!(
2865                        edge.edge,
2866                        bloq_ir::BloqEdge::Value {
2867                            output: bloq_ir::ObservableOutput::Flip,
2868                            ..
2869                        }
2870                    )
2871                })
2872                .collect::<Vec<_>>();
2873            assert!(
2874                !flips.is_empty(),
2875                "proxy frames consume public decoder flips"
2876            );
2877            for edge in flips {
2878                assert!(matches!(
2879                    bloq[edge.source].try_classical(),
2880                    Some(bloq_ir::ClassicalNode::Observable { index: Some(_), .. })
2881                ));
2882                assert!(matches!(
2883                    bloq[edge.target].try_classical(),
2884                    Some(bloq_ir::ClassicalNode::Observable { index: None, .. })
2885                ));
2886            }
2887        }
2888    }
2889
2890    #[test]
2891    fn random_clifford_proxy_is_seeded_and_records_its_seed() {
2892        let hierarchical = GalleryItem::T.build();
2893        for graph in [hierarchical.flatten().unwrap(), hierarchical] {
2894            let compile = || {
2895                compile_random_clifford_proxy(CompileConfig::default(), &graph, 0xC10F_F0AD)
2896                    .expect("random proxy compiles")
2897                    .bloq
2898            };
2899            let first = compile();
2900            let second = compile();
2901            assert_eq!(first.to_binary(), second.to_binary());
2902            assert_eq!(
2903                first.metadata().get(CLIFFORD_PROXY_SEED_METADATA_KEY),
2904                Some(&bloq_ir::MetadataValue::U64(0xC10F_F0AD))
2905            );
2906            bloq_stim::emit_bloq_stim(&first).expect("random proxy is static Stim");
2907        }
2908    }
2909
2910    #[test]
2911    fn distance_proxy_replaces_t_even_when_mpp_preparation_is_enabled() {
2912        let bloq = compile_clifford_proxy(
2913            CompileConfig::default().with_prepare_t_with_mpps(true),
2914            &GalleryItem::T
2915                .build()
2916                .flatten()
2917                .expect("gallery graph expands"),
2918            &[false],
2919        )
2920        .expect("MPP-configured T proxy compiles")
2921        .bloq;
2922
2923        assert!(bloq.nodes().all(|(_, node)| node.try_region().is_none()));
2924        bloq_stim::emit_bloq_stim(&bloq).expect("MPP-configured T proxy is static Stim");
2925    }
2926
2927    #[test]
2928    fn clifford_proxy_accepts_partial_padding() {
2929        let mut bloq = compile_clifford_proxy(
2930            CompileConfig::default(),
2931            &GalleryItem::T
2932                .build()
2933                .flatten()
2934                .expect("gallery graph expands"),
2935            &[false],
2936        )
2937        .expect("T proxy compiles")
2938        .bloq;
2939        let target = bloq
2940            .nodes()
2941            .find_map(|(id, node)| {
2942                node.block_members()
2943                    .iter()
2944                    .any(|member| member.pos == ivec3(1, 0, 2))
2945                    .then_some(id)
2946            })
2947            .expect("pinned selective node exists");
2948        let source = bloq
2949            .incoming(target)
2950            .find_map(|edge| {
2951                matches!(edge.edge, bloq_ir::BloqEdge::Quantum(_)).then_some(edge.source)
2952            })
2953            .expect("pinned selective seam exists");
2954
2955        let padding = bloq
2956            .insert_memory_rounds(
2957                MemoryRoundTarget::Edge {
2958                    path: LevelPath::default(),
2959                    from: source,
2960                    to: target,
2961                },
2962                10,
2963            )
2964            .expect("pinned selective seam accepts decoder wait");
2965
2966        assert_eq!(bloq[padding].memory_rounds(), Some(10));
2967        bloq.validate().expect("padded proxy validates");
2968        bloq_stim::emit_bloq_stim(&bloq).expect("padded proxy emits");
2969    }
2970
2971    #[test]
2972    fn clifford_proxy_rejects_wrong_selective_pin_count() {
2973        let mut graph = BlockGraph::new();
2974        graph.add_block(Block::new(
2975            IVec3::ZERO,
2976            BlockKind::Selective(bloq_graph::SelectiveKind::XY),
2977        ));
2978
2979        assert!(matches!(
2980            compile_clifford_proxy(CompileConfig::default(), &graph, &[]),
2981            Err(CompileError::CliffordProxyPinCount {
2982                expected: 1,
2983                actual: 0
2984            })
2985        ));
2986        assert!(matches!(
2987            compile_clifford_proxy(CompileConfig::default(), &graph, &[false, true]),
2988            Err(CompileError::CliffordProxyPinCount {
2989                expected: 1,
2990                actual: 2
2991            })
2992        ));
2993    }
2994
2995    /// A graph with T blocks at `positions`, each under its own ZXZ cube.
2996    fn t_blocks_graph(positions: &[IVec3]) -> BlockGraph {
2997        let mut graph = BlockGraph::new();
2998        for &pos in positions {
2999            graph.add_block(Block::new(pos, BlockKind::T));
3000            graph.add_block(Block::new(pos + IVec3::Z, BlockKind::Cube(CubeKind::ZXZ)));
3001            graph.add_pipe(bloq_graph::Pipe::new(pos, bloq_graph::Direction::ZPLUS));
3002        }
3003        graph
3004    }
3005
3006    /// Compile a graph's block templates only (no lowering), returning the pool
3007    /// size — the Step-3 surface while T lowering lands separately.
3008    fn compiled_template_pool_len(graph: &BlockGraph) -> usize {
3009        let ctx = CompileContext::new(CompileConfig::new(3));
3010        let signatures = block_signatures(
3011            graph,
3012            &join_component_layer_schedules(graph).expect("no mixed spatial Hadamard walls"),
3013            ctx.config.code_distance(),
3014        )
3015        .expect("valid graph");
3016        let blocks = ordered_blocks(graph);
3017        ctx.compile_blocks(graph, &blocks, &signatures)
3018            .expect("compile blocks");
3019        ctx.cache.pool_len()
3020    }
3021
3022    #[test]
3023    fn t_blocks_with_matching_signatures_share_cached_templates() {
3024        // Two far-apart T blocks: same connectivity, boundary basis, and spill
3025        // side => one cultivation + escape pair, plus the shared cube template.
3026        let graph = t_blocks_graph(&[ivec3(0, 0, 0), ivec3(5, 0, 0)]);
3027        assert_eq!(compiled_template_pool_len(&graph), 3);
3028    }
3029
3030    #[test]
3031    fn t_blocks_with_differing_surgery_sides_compile_distinct_templates() {
3032        // Occupy the second T's -y neighbor so its spill side falls through to
3033        // +x: a different signature, so a second cultivation + escape pair.
3034        // Pool: piped cube + isolated boxing cube + 2 × (cultivation, escape).
3035        let mut graph = t_blocks_graph(&[ivec3(0, 0, 0), ivec3(5, 0, 0)]);
3036        graph.add_block(Block::new(ivec3(5, -1, 0), BlockKind::Cube(CubeKind::ZXZ)));
3037        assert_eq!(compiled_template_pool_len(&graph), 6);
3038    }
3039
3040    #[test]
3041    fn t_region_body_carries_seam_restarts_and_gap_wiring() {
3042        use bloq_ir::{BloqNodeKind, ClassicalExpr, ClassicalNode, ObservableOutput, RegionNode};
3043
3044        let graph = t_blocks_graph(&[ivec3(0, 0, 0)]);
3045        let ctx = CompileContext::new(CompileConfig::new(3));
3046        let program = ctx.compile(&graph).expect("T graph compiles").bloq;
3047        program.validate().expect("retry wiring is valid IR");
3048
3049        let (body, condition, source) = program
3050            .nodes()
3051            .find_map(|(_, node)| match &node.kind {
3052                BloqNodeKind::Region(RegionNode::RepeatUntilSuccess {
3053                    body,
3054                    restart_condition,
3055                    restart_source,
3056                }) => Some((
3057                    body,
3058                    restart_condition,
3059                    restart_source.expect("explicit body retry predicate"),
3060                )),
3061                _ => None,
3062            })
3063            .expect("the T block lowers to one RUS region");
3064
3065        // Two quantum stage nodes: cultivation (restart-free side tables — its
3066        // template-local restarts ride the template) then escape, whose node
3067        // carries the intra-body seam restarts. At least one seam restart spans
3068        // both stage instances (the cultivation-closure chains).
3069        let quantum: Vec<_> = body.quantum_nodes().map(|(_, node)| node).collect();
3070        assert_eq!(quantum.len(), 2, "cultivation + escape stage nodes");
3071        let (cultivation, escape) = (quantum[0], quantum[1]);
3072        let cultivation_id = cultivation.instances[0].id;
3073        let escape_id = escape.instances[0].id;
3074        assert!(cultivation.restarts.is_empty());
3075        assert!(!escape.restarts.is_empty(), "intra-body seam restarts");
3076        assert!(
3077            escape.restarts.iter().any(|restart| {
3078                let instances: crate::FxSet<_> = restart
3079                    .parity
3080                    .measurements()
3081                    .map(|measurement| measurement.instance)
3082                    .collect();
3083                instances.contains(&cultivation_id) && instances.contains(&escape_id)
3084            }),
3085            "a cross-template restart chain spans both stage instances"
3086        );
3087
3088        let mut indices = body
3089            .nodes()
3090            .filter_map(|(_, node)| match node.try_classical() {
3091                Some(ClassicalNode::Observable {
3092                    index: Some(index),
3093                    measurements,
3094                    operators,
3095                }) => {
3096                    assert!(
3097                        measurements
3098                            .iter()
3099                            .all(|record| record.instance == escape_id)
3100                    );
3101                    assert!(
3102                        operators
3103                            .iter()
3104                            .all(|operator| operator.instance == escape_id)
3105                    );
3106                    assert!(!measurements.is_empty());
3107                    Some(*index)
3108                }
3109                _ => None,
3110            })
3111            .collect::<Vec<_>>();
3112        indices.sort_unstable();
3113        assert_eq!(indices.len(), 2, "two GAP decoding problems");
3114        assert_ne!(indices[0], indices[1]);
3115        let computes = body
3116            .nodes()
3117            .filter_map(|(id, node)| match node.try_classical() {
3118                Some(ClassicalNode::Compute { expr }) => Some((id, expr)),
3119                _ => None,
3120            })
3121            .collect::<Vec<_>>();
3122        assert_eq!(computes.len(), 1, "one explicit retry predicate");
3123        let (retry, expr) = computes[0];
3124        assert_eq!(source.node, retry);
3125        assert_eq!(source.output, ObservableOutput::Corrected);
3126        assert_ne!(
3127            body.value_output(),
3128            Some(source),
3129            "retry is not the body result"
3130        );
3131        assert_eq!(*condition, ClassicalExpr::In(0));
3132        let mut inputs = body.value_inputs(retry).collect::<Vec<_>>();
3133        inputs.sort_unstable_by_key(|input| input.slot);
3134        assert_eq!(
3135            inputs.iter().map(|input| input.slot).collect::<Vec<_>>(),
3136            [0, 1]
3137        );
3138        assert_ne!(inputs[0].producer, inputs[1].producer);
3139        for input in inputs {
3140            assert_eq!(input.output, Some(ObservableOutput::Flip));
3141            assert!(matches!(
3142                body[input.producer].try_classical(),
3143                Some(ClassicalNode::Observable { index: Some(_), .. })
3144            ));
3145        }
3146        for (flips, expected) in [
3147            ([false, false], false),
3148            ([false, true], true),
3149            ([true, false], true),
3150            ([true, true], true),
3151        ] {
3152            let retry = expr
3153                .eval(&mut |slot| flips.get(slot as usize).copied())
3154                .unwrap();
3155            assert_eq!(retry, expected, "decoder flips {flips:?}");
3156            assert_eq!(
3157                condition.eval(&mut |slot| (slot == 0).then_some(retry)),
3158                Some(expected)
3159            );
3160        }
3161
3162        // The downstream +Z neighbour closes the escape end face: its top-level
3163        // detectors reference the in-body escape instance, and nothing at top
3164        // level restarts (that would trip RestartOutsideRepeatUntilSuccess).
3165        let top_quantum: Vec<_> = program.quantum_nodes().map(|(_, node)| node).collect();
3166        assert!(top_quantum.iter().all(|node| node.restarts.is_empty()));
3167        assert!(
3168            top_quantum.iter().any(|node| {
3169                program.node_detectors(node).unwrap().any(|detector| {
3170                    detector
3171                        .measurements()
3172                        .any(|measurement| measurement.instance == escape_id)
3173                })
3174            }),
3175            "the neighbour's seam detectors reference the in-body escape instance"
3176        );
3177    }
3178
3179    #[test]
3180    fn boxed_t_block_needs_free_neighbor_end_to_end() {
3181        let mut graph = t_blocks_graph(&[ivec3(0, 0, 0)]);
3182        for neighbor in [
3183            ivec3(0, -1, 0),
3184            ivec3(1, 0, 0),
3185            ivec3(0, 1, 0),
3186            ivec3(-1, 0, 0),
3187        ] {
3188            graph.add_block(Block::new(neighbor, BlockKind::Cube(CubeKind::ZXZ)));
3189        }
3190        let ctx = CompileContext::new(CompileConfig::new(3));
3191        assert!(matches!(
3192            ctx.compile_and_validate(&graph).unwrap_err(),
3193            CompileError::TBlockNeedsFreeNeighbor { .. }
3194        ));
3195        assert!(matches!(
3196            ctx.compile(&graph).unwrap_err(),
3197            CompileError::TBlockNeedsFreeNeighbor { .. }
3198        ));
3199    }
3200
3201    #[test]
3202    fn compile_t_block_lowers_to_repeat_until_success_region() {
3203        use bloq_ir::{BloqNodeKind, RegionNode};
3204
3205        let graph = t_blocks_graph(&[ivec3(0, 0, 0)]);
3206        let ctx = CompileContext::new(CompileConfig::new(3));
3207
3208        let program = ctx.compile(&graph).expect("T graph compiles").bloq;
3209
3210        let rus_count = program
3211            .nodes()
3212            .filter(|(_, node)| {
3213                matches!(
3214                    node.kind,
3215                    BloqNodeKind::Region(RegionNode::RepeatUntilSuccess { .. })
3216                )
3217            })
3218            .count();
3219        assert_eq!(rus_count, 1, "the T block is one RepeatUntilSuccess region");
3220
3221        // Audit the complete lowered IR explicitly, including the RUS body.
3222        program.validate().expect("T program validates recursively");
3223
3224        // The static Stim backend rejects the region by name; the native
3225        // dynamic runtime executes it.
3226        let error = bloq_stim::emit_bloq_stim(&program).expect_err("RUS is backend-rejected");
3227        assert!(
3228            matches!(error, bloq_stim::StimEmissionError::UnsupportedNode(reason) if reason.contains("RepeatUntilSuccess")),
3229            "expected RepeatUntilSuccess rejection, got: {error:?}",
3230        );
3231    }
3232
3233    #[test]
3234    fn registry_closed_readouts_keep_their_code_distance() {
3235        for gallery in [
3236            GalleryItem::XMemory,
3237            GalleryItem::YMemory,
3238            GalleryItem::Stability,
3239        ] {
3240            let context = CompileContext::new(CompileConfig::default());
3241            let bloq = registry::compile_graph(
3242                &context,
3243                &gallery.build().flatten().expect("gallery graph expands"),
3244            )
3245            .unwrap()
3246            .bloq;
3247            let noise = stim::noise::UniformDepolarizing::new(1e-3).unwrap();
3248            let circuit: stim::Circuit = bloq_stim::emit_bloq_stim_with_stim_noise(&bloq, &noise)
3249                .unwrap()
3250                .parse()
3251                .unwrap();
3252            assert_eq!(circuit.num_observables(), 1, "{gallery:?}");
3253            assert_eq!(
3254                circuit.shortest_graphlike_error().unwrap().len(),
3255                3,
3256                "{gallery:?}"
3257            );
3258        }
3259    }
3260
3261    #[test]
3262    fn registry_preparations_do_not_decode_unavailable_output_axes() {
3263        for gallery in [GalleryItem::BellState, GalleryItem::GHZ] {
3264            let graph = gallery.build().flatten().expect("gallery graph expands");
3265            let expected = bloq_graph::ZXGraph::from_block_graph_for_analysis(&graph)
3266                .unwrap()
3267                .stabilizers()
3268                .unwrap()
3269                .generators
3270                .len();
3271            let context = CompileContext::new(CompileConfig::default());
3272            for bloq in [
3273                registry::compile_graph(&context, &graph).unwrap().bloq,
3274                registry::compile_hierarchy(&context, &gallery.build(), NonZeroUsize::MIN)
3275                    .unwrap()
3276                    .bloq,
3277            ] {
3278                let noise = stim::noise::UniformDepolarizing::new(1e-3).unwrap();
3279                let circuit: stim::Circuit =
3280                    bloq_stim::emit_bloq_stim_with_stim_noise(&bloq, &noise)
3281                        .unwrap()
3282                        .parse()
3283                        .unwrap();
3284                assert_eq!(circuit.num_observables(), expected as u64, "{gallery:?}");
3285                assert_eq!(circuit.shortest_graphlike_error().unwrap().len(), 3);
3286            }
3287        }
3288    }
3289
3290    #[test]
3291    fn empty_native_programs_compile_and_validate() {
3292        let context = CompileContext::new(CompileConfig::default());
3293        let graph = BlockGraph::new();
3294        for bloq in [
3295            context.compile_and_validate(&graph).unwrap().bloq,
3296            context
3297                .compile(&graph.with_inferred_interface().unwrap())
3298                .unwrap()
3299                .bloq,
3300        ] {
3301            bloq.validate().unwrap();
3302            assert_eq!(bloq.node_count(), 0);
3303        }
3304    }
3305
3306    #[test]
3307    fn t_gallery_programs_compile_validate_and_are_backend_rejected() {
3308        // The U17 §7 acceptance matrix across the three d_int regimes (d=3
3309        // surgery-at-3, d=5 surgery-at-5, d=7 first 5→7 expansion).
3310        // The gallery graphs compile as-is: their In/Out `Port` blocks lower as
3311        // ideal temporal seams (`fill_ports_auto` is wrong here — it would fill
3312        // the selective/T port-like positions too and break the resolve actions).
3313        for gallery in [
3314            GalleryItem::T,
3315            GalleryItem::TWithPreparedY,
3316            GalleryItem::PhaseGradientK4,
3317            GalleryItem::THTH,
3318        ] {
3319            let graph = gallery.build().flatten().expect("gallery graph expands");
3320            // Backend-rejection is distance-invariant (RUS and dynamic membership are
3321            // rejected regardless of code distance); distance coverage lives in
3322            // the proxy-distance tests. One distance suffices here.
3323            {
3324                let code_distance = 3;
3325                let ctx = CompileContext::new(CompileConfig::new(code_distance));
3326                let program = ctx
3327                    .compile(&graph)
3328                    .unwrap_or_else(|error| {
3329                        panic!("{gallery:?} d={code_distance} compiles: {error}")
3330                    })
3331                    .bloq;
3332                program.validate().unwrap_or_else(|error| {
3333                    panic!("{gallery:?} d={code_distance} validates: {error}")
3334                });
3335                let error = bloq_stim::emit_bloq_stim(&program)
3336                    .expect_err("T programs are backend-rejected");
3337                assert!(
3338                    matches!(&error, bloq_stim::StimEmissionError::UnsupportedNode(_)),
3339                    "{gallery:?} d={code_distance}: expected region rejection, got {error:?}",
3340                );
3341            }
3342        }
3343    }
3344
3345    #[test]
3346    fn phase_gradient_records_padding_before_selective_components() {
3347        use bloq_ir::BloqEdge;
3348
3349        let ctx = CompileContext::new(CompileConfig::new(3));
3350        let program = ctx
3351            .compile(
3352                &GalleryItem::PhaseGradientK4
3353                    .build()
3354                    .flatten()
3355                    .expect("gallery graph expands"),
3356            )
3357            .expect("phase gradient compiles")
3358            .bloq;
3359        let mut seams = 0;
3360        for (branch, node) in program.nodes() {
3361            if !node
3362                .try_quantum()
3363                .is_some_and(|quantum| !quantum.guards.is_empty())
3364            {
3365                continue;
3366            }
3367            for edge in program.incoming(branch) {
3368                let BloqEdge::Quantum(quantum) = edge.edge else {
3369                    continue;
3370                };
3371                seams += 1;
3372                assert!(
3373                    quantum.pipes.iter().all(|seam| seam.padding.is_some()),
3374                    "selective input {edge:?} must persist a padding template per pipe",
3375                );
3376            }
3377        }
3378        assert!(seams > 0, "phase gradient contains adaptive quantum seams");
3379    }
3380
3381    /// Exchange-format round trips on real compiled programs:
3382    /// both codecs must reproduce the program exactly — checked as binary
3383    /// byte equality (full structural identity, node-id holes included), a
3384    /// text fixpoint, a passing re-validation, and byte-identical Stim
3385    /// emission where the static backend supports the program.
3386    #[rstest]
3387    #[case(GalleryItem::CNOT)]
3388    #[case(GalleryItem::S)]
3389    #[case(GalleryItem::T)]
3390    fn exchange_formats_round_trip(#[case] gallery: GalleryItem) {
3391        let ctx = CompileContext::new(CompileConfig::new(3));
3392        let program = ctx
3393            .compile(&gallery.build().flatten().expect("gallery graph expands"))
3394            .expect("compiles")
3395            .bloq;
3396
3397        let bytes = program.to_binary();
3398        let from_binary = bloq_ir::Bloq::from_binary(&bytes).expect("binary decodes");
3399        assert_eq!(
3400            bytes,
3401            from_binary.to_binary(),
3402            "{gallery:?}: binary fixpoint"
3403        );
3404
3405        let text = program.to_text();
3406        let from_text = bloq_ir::Bloq::from_text(&text)
3407            .unwrap_or_else(|error| panic!("{gallery:?}: text parses: {error}"));
3408        assert_eq!(
3409            bytes,
3410            from_text.to_binary(),
3411            "{gallery:?}: text round trip is structurally lossless"
3412        );
3413        assert_eq!(text, from_text.to_text(), "{gallery:?}: text fixpoint");
3414
3415        from_text
3416            .validate()
3417            .unwrap_or_else(|error| panic!("{gallery:?}: restored program validates: {error}"));
3418
3419        let expected = bloq_stim::emit_bloq_stim(&program);
3420        let restored = bloq_stim::emit_bloq_stim(&from_text);
3421        if gallery == GalleryItem::T {
3422            for result in [expected, restored] {
3423                assert!(matches!(
3424                    result,
3425                    Err(bloq_stim::StimEmissionError::UnsupportedNode(_))
3426                ));
3427            }
3428        } else {
3429            assert_eq!(
3430                expected.expect("static original emits"),
3431                restored.expect("static restored program emits"),
3432                "{gallery:?}: identical Stim emission"
3433            );
3434        }
3435    }
3436
3437    /// Feedback frames consume corrected observables and retain physical record
3438    /// dependencies. Action-free programs use the same frame construction.
3439    #[test]
3440    fn s_gallery_lowers_output_frames_from_symbolic_solve() {
3441        use bloq_ir::ClassicalNode;
3442
3443        let ctx = CompileContext::new(CompileConfig::default());
3444        let program = ctx
3445            .compile(
3446                &GalleryItem::S
3447                    .build()
3448                    .flatten()
3449                    .expect("gallery graph expands"),
3450            )
3451            .unwrap()
3452            .bloq;
3453        let frames = program.output_frames();
3454        assert_eq!(frames.len(), 1, "S teleport has one output port");
3455
3456        let classical = |node: bloq_ir::BloqNodeId| {
3457            program[node]
3458                .try_classical()
3459                .expect("frame nodes are classical")
3460                .clone()
3461        };
3462        assert!(matches!(
3463            classical(frames[0].x),
3464            ClassicalNode::Compute { .. }
3465        ));
3466        assert!(matches!(
3467            classical(frames[0].z),
3468            ClassicalNode::Compute { .. }
3469        ));
3470
3471        // The Z frame reaches a complete observable and its records, through
3472        // any remaining shared fragments or fused computations.
3473        let mut stack = vec![frames[0].z];
3474        let mut seen = std::collections::HashSet::new();
3475        let (mut saw_records, mut saw_observable) = (false, false);
3476        while let Some(node) = stack.pop() {
3477            if !seen.insert(node) {
3478                continue;
3479            }
3480            if let Some(classical) = program[node].try_classical() {
3481                saw_records |= !classical.measurements().is_empty();
3482                saw_observable |= matches!(classical, ClassicalNode::Observable { .. });
3483            }
3484            stack.extend(program.top().data_inputs(node).map(|input| input.producer));
3485        }
3486        assert!(
3487            saw_observable,
3488            "Z frame reads a member's corrected observable"
3489        );
3490        assert!(
3491            saw_records,
3492            "the observable recipe contains physical records"
3493        );
3494
3495        let ctx = CompileContext::new(CompileConfig::default());
3496        let action_free = ctx
3497            .compile(
3498                &GalleryItem::CNOT
3499                    .build()
3500                    .flatten()
3501                    .expect("gallery graph expands"),
3502            )
3503            .unwrap()
3504            .bloq;
3505        assert_eq!(
3506            action_free.output_frames().len(),
3507            2,
3508            "action-free CNOT gets one FramePair per output port"
3509        );
3510    }
3511
3512    #[test]
3513    fn output_frame_ports_match_logical_outputs() {
3514        let compile = |graph: &BlockGraph| {
3515            CompileContext::new(CompileConfig::new(3))
3516                .compile(graph)
3517                .expect("gallery graph compiles")
3518                .bloq
3519        };
3520
3521        let cnot = compile(
3522            &GalleryItem::CNOT
3523                .build()
3524                .flatten()
3525                .expect("gallery graph expands"),
3526        );
3527        let frame_outputs: Vec<_> = cnot
3528            .output_frames()
3529            .into_iter()
3530            .map(|frame| frame.port)
3531            .collect();
3532        let logical_outputs: Vec<_> = cnot
3533            .logical_outputs()
3534            .iter()
3535            .map(|output| output.port)
3536            .collect();
3537        assert_eq!(frame_outputs, logical_outputs);
3538        assert!(frame_outputs.contains(&glam::ivec3(1, 1, 3)));
3539
3540        let shifted_t = GalleryItem::T
3541            .build()
3542            .flatten()
3543            .expect("gallery graph expands")
3544            .shift_positions(IVec3::X)
3545            .expect("shift fits");
3546        let t = compile(&shifted_t);
3547        assert_eq!(
3548            t.output_frames()
3549                .into_iter()
3550                .map(|frame| frame.port)
3551                .collect::<Vec<_>>(),
3552            t.logical_outputs()
3553                .iter()
3554                .map(|output| output.port)
3555                .collect::<Vec<_>>(),
3556            "selective-fill frames use the same source output ports"
3557        );
3558    }
3559
3560    #[test]
3561    fn compile_graph_lowers_single_cube() {
3562        let graph = single_cube_runtime_graph();
3563        let ctx = CompileContext::new(CompileConfig::new(3));
3564
3565        let artifacts = ctx.compile(&graph).unwrap();
3566        let program = artifacts.bloq;
3567
3568        assert_eq!(program.quantum_node_count(), 1);
3569        assert_eq!(program.templates().len(), 1);
3570        let node = program.quantum_nodes().next().unwrap().1;
3571        assert_eq!(node.instances.len(), 1);
3572        assert_eq!(node.instances[0].id.0, 0);
3573        assert_eq!(node.instances[0].template_id.0, 0);
3574        assert_eq!(node.timeline, None);
3575        // The cube's logical observable lowered to a classical Observable node.
3576        assert!(program.nodes().any(|(_, node)| matches!(
3577            node.try_classical(),
3578            Some(bloq_ir::ClassicalNode::Observable { .. })
3579        )));
3580    }
3581
3582    /// Minimal selective fixture; its input port makes both cap arms reachable.
3583    fn selective_measurement_graph() -> BlockGraph {
3584        BlockGraph::from_blog_text(
3585            "BLOG 1.0\n\n  0: Port [0,0,-1]\n  1: ZXZ [0,0,0]\n  2: XZ [0,0,1]\n  3: ZXZ [1,0,0]\n  [0,0,-1] -> +Z\n  [0,0,0] -> +Z\n\n  m2 = measure 3\n  resolve 2 if m2\n",
3586        )
3587        .expect("selective measurement graph parses")
3588    }
3589
3590    #[test]
3591    fn compile_selective_block_lowers_to_guarded_component() {
3592        let graph = selective_measurement_graph();
3593        let ctx = CompileContext::new(CompileConfig::new(3));
3594        let program = ctx.compile(&graph).expect("selective graph compiles").bloq;
3595
3596        assert_eq!(program.quantum_node_count(), 4);
3597        assert!(program.nodes().all(|(_, node)| node.try_region().is_none()));
3598        assert!(ctx.cache.pool_len() >= 3);
3599        program.validate().unwrap();
3600        assert!(matches!(
3601            bloq_stim::emit_bloq_stim(&program),
3602            Err(bloq_stim::StimEmissionError::UnsupportedNode(_))
3603        ));
3604
3605        let selector_name = bloq_graph::selective_selector_name(ivec3(0, 0, 2));
3606        let mut arm_ids = Vec::new();
3607        for choice in [false, true] {
3608            let pinned = program
3609                .pin_membership(&std::collections::BTreeMap::from([(
3610                    selector_name.clone(),
3611                    choice,
3612                )]))
3613                .expect("both selective choices are reachable");
3614            assert!(!pinned.has_conditional_membership());
3615            let (owner, quantum) = pinned
3616                .quantum_nodes()
3617                .find(|(owner, _)| {
3618                    pinned[*owner]
3619                        .block_members()
3620                        .iter()
3621                        .any(|member| member.pos == ivec3(0, 0, 2))
3622                })
3623                .unwrap();
3624            assert_eq!(quantum.instances.len(), 1);
3625            arm_ids.push(quantum.instances[0].id);
3626            pinned[owner]
3627                .instantiate_circuit(pinned.templates())
3628                .unwrap();
3629
3630            // Each pinned seam reads its predecessor and the selected arm's records.
3631            let arm = quantum.instances[0].id;
3632            assert!(pinned.node_detector_count(quantum).unwrap() > 0);
3633            for detector in pinned.node_detectors(quantum).unwrap() {
3634                let instances = detector
3635                    .measurements()
3636                    .map(|record| record.instance)
3637                    .collect::<Vec<_>>();
3638                assert!(instances.contains(&arm));
3639                assert!(instances.iter().any(|&instance| instance != arm));
3640            }
3641        }
3642        assert_ne!(arm_ids[0], arm_ids[1]);
3643    }
3644
3645    #[test]
3646    fn selective_pins_preserve_shared_compound_controls() {
3647        use std::collections::BTreeMap;
3648
3649        let graph = BlockGraph::from_blog_text(
3650            "BLOG 1.0\n\n  0: Port [0,0,0]\n  1: XZ [0,0,1]\n  2: Port [2,0,0]\n  3: XZ [2,0,1]\n  4: ZXZ [4,0,0]\n  5: ZXZ [6,0,0]\n  [0,0,0] -> +Z\n  [2,0,0] -> +Z\n\n  a = measure 4\n  b = measure 5\n  resolve 1 if a ^ b\n  resolve 3 if a ^ b\n",
3651        ).unwrap();
3652        let program = compile(&graph, 3).unwrap();
3653        let names = [ivec3(0, 0, 1), ivec3(2, 0, 1)].map(bloq_graph::selective_selector_name);
3654        for choice in [false, true] {
3655            let pinned = program
3656                .pin_membership(&BTreeMap::from([
3657                    (names[0].clone(), choice),
3658                    (names[1].clone(), choice),
3659                ]))
3660                .unwrap();
3661            assert!(!pinned.has_conditional_membership());
3662            pinned.validate().unwrap();
3663        }
3664        assert!(matches!(
3665            program.pin_membership(&BTreeMap::from([
3666                (names[0].clone(), false),
3667                (names[1].clone(), true),
3668            ])),
3669            Err(bloq_ir::MembershipPinError::UnreachableAssignment)
3670        ));
3671    }
3672
3673    /// A logical operator threaded through a selective measurement: a `-Z`-boundary
3674    /// port initializes it, the selective `XZ` block measures it, and `m2` (a side
3675    /// cube) drives the resolve. Unlike [`selective_measurement_graph`] (whose only
3676    /// generator, `m2`, does not touch the selective site), here a `Logical`
3677    /// generator crosses the selective block, exercising the branch-conditioned
3678    /// observable lowering.
3679    fn selective_crossing_graph() -> BlockGraph {
3680        BlockGraph::from_blog_text(
3681            "BLOG 1.0\n\n  0: Port [0,0,0]\n  1: XZ [0,0,1]\n  2: ZXZ [1,0,0]\n  [0,0,0] -> +Z\n\n  m2 = measure 2\n  resolve 1 if m2\n",
3682        )
3683        .expect("selective crossing graph parses")
3684    }
3685
3686    fn value_with_decode(program: &Bloq, id: bloq_ir::BloqNodeId, control: bool) -> bool {
3687        use bloq_ir::ClassicalNode;
3688
3689        match program[id].try_classical().expect("classical fold") {
3690            ClassicalNode::Observable { .. } => control,
3691            ClassicalNode::Compute { expr } => expr
3692                .eval(&mut |slot| {
3693                    let input = program
3694                        .top()
3695                        .value_inputs(id)
3696                        .find(|input| input.slot == slot)?;
3697                    Some(value_with_decode(program, input.producer, control))
3698                })
3699                .expect("all fold inputs are available"),
3700            other => panic!("unexpected fold producer: {other:?}"),
3701        }
3702    }
3703
3704    #[test]
3705    fn feedback_folding_respects_the_resolve_condition() {
3706        use bloq_ir::{ClassicalNode, ValueRole};
3707
3708        let graph = BlockGraph::from_blog_text(
3709            "BLOG 1.0\n\n  0: Port [0,0,0]\n  1: XZ [0,0,1]\n  2: ZXZ [1,0,0]\n  [0,0,0] -> +Z\n\n  m2 = measure 2\n  resolve 1 if m2\n  feedback X 1 if m2\n",
3710        )
3711        .expect("delta-only feedback graph parses");
3712        let ctx = CompileContext::new(CompileConfig::new(3));
3713        let program = ctx
3714            .compile(&graph)
3715            .expect("delta-only feedback compiles")
3716            .bloq;
3717        let folds: Vec<_> = program.nodes()
3718            .filter(|(_, node)| matches!(node.try_classical(), Some(ClassicalNode::Observable { index, .. }) if *index != Some(0)))
3719            .flat_map(|(observable, _)| program.incoming(observable))
3720            .filter(|edge| matches!(edge.edge, bloq_ir::BloqEdge::Value { role: ValueRole::FeedbackFold { .. }, .. }))
3721            .collect();
3722
3723        // False selects Z, but disables feedback. True enables X feedback on
3724        // the X arm, where it commutes. The total fold is zero in both cases.
3725        for control in [false, true] {
3726            assert!(!folds.iter().fold(false, |odd, edge| {
3727                odd ^ value_with_decode(&program, edge.source, control)
3728            }));
3729        }
3730    }
3731
3732    /// Both selector values retain the selected input operator and read only
3733    /// the active arm's records, independent of how the Boolean rows are split.
3734    #[test]
3735    fn selective_crossing_observable_reads_selected_arm() {
3736        use bloq_graph::Pauli;
3737        use bloq_ir::{BloqEdge, ClassicalNode};
3738        use std::collections::BTreeSet;
3739
3740        let program = CompileContext::new(CompileConfig::default())
3741            .compile(&selective_crossing_graph())
3742            .unwrap()
3743            .bloq;
3744        program.validate().unwrap();
3745        let (branch, quantum) = program
3746            .quantum_nodes()
3747            .find(|(_, quantum)| quantum.instances.len() == 2 && !quantum.guards.is_empty())
3748            .unwrap();
3749        let arms = [false, true].map(|control| {
3750            let selected = program[branch]
3751                .select_quantum_members(|slot| {
3752                    let input = program
3753                        .value_inputs(branch)
3754                        .find(|input| input.slot == slot)?;
3755                    Some(value_with_decode(&program, input.producer, control))
3756                })
3757                .unwrap();
3758            selected.expect_quantum().instances[0].id
3759        });
3760        assert_eq!(quantum.instances.len(), arms.len());
3761        for control in [false, true] {
3762            let mut selected = 0;
3763            for (observable, _) in program.nodes().filter(|(_, node)| {
3764                matches!(node.try_classical(), Some(ClassicalNode::Observable { index, .. }) if *index != Some(0))
3765            }) {
3766                let mut bases = Vec::new();
3767                let mut records = BTreeSet::new();
3768                for input in program.top().data_inputs(observable).filter(|input| input.output.is_none()) {
3769                    let producer = input.producer;
3770                    let node = &program[producer];
3771                    if let Some(slot) = node.activation {
3772                        let guard = program.value_inputs(producer).find(|input| input.slot == slot).unwrap().producer;
3773                        if !value_with_decode(&program, guard, control) { continue; }
3774                    }
3775                    if let Some(ClassicalNode::Observable { measurements, operators, .. }) = node.try_classical() {
3776                        bases.extend(operators.iter().flat_map(|operator| operator.operator.iter().map(|(_, pauli)| *pauli)));
3777                        records.extend(measurements.iter().map(|record| record.instance));
3778                        if measurements.iter().any(|record| arms.contains(&record.instance)) {
3779                            assert!(program.incoming(producer).any(|edge| edge.source == branch && *edge.edge == BloqEdge::Order));
3780                        }
3781                    }
3782                }
3783                if bases.is_empty() { continue; }
3784                selected += 1;
3785                assert!(bases.iter().all(|&basis| basis == if control { Pauli::X } else { Pauli::Z }));
3786                assert!(records.contains(&arms[usize::from(control)]));
3787                assert!(!records.contains(&arms[usize::from(!control)]));
3788            }
3789            assert_eq!(selected, 1, "one selected input correlation per arm");
3790        }
3791        assert!(matches!(
3792            bloq_stim::emit_bloq_stim(&program),
3793            Err(bloq_stim::StimEmissionError::UnsupportedNode(_))
3794        ));
3795    }
3796
3797    #[test]
3798    fn compile_lowers_observable_to_classical_nodes() {
3799        use bloq_ir::{BloqEdge, ClassicalNode};
3800
3801        let graph = single_cube_runtime_graph();
3802        let ctx = CompileContext::new(CompileConfig::new(3));
3803        let program = ctx.compile(&graph).unwrap().bloq;
3804        let g = &program;
3805
3806        assert_eq!(program.quantum_node_count(), 1);
3807
3808        let observables = g
3809            .nodes()
3810            .filter_map(|(id, node)| match node.try_classical() {
3811                Some(ClassicalNode::Observable { measurements, .. })
3812                    if !measurements.is_empty() =>
3813                {
3814                    Some(id)
3815                }
3816                _ => None,
3817            })
3818            .collect::<Vec<_>>();
3819        assert!(
3820            !observables.is_empty(),
3821            "records belong directly to complete observables"
3822        );
3823        let order = program.deterministic_emit_order().expect("acyclic");
3824        let position = |id| order.iter().position(|&node| node == id).unwrap();
3825        for observable in observables {
3826            let owners = g
3827                .incoming(observable)
3828                .filter(|edge| {
3829                    matches!(edge.edge, BloqEdge::Order) && g[edge.source].try_quantum().is_some()
3830                })
3831                .collect::<Vec<_>>();
3832            assert!(
3833                !owners.is_empty(),
3834                "the complete readout waits for producing quantum work"
3835            );
3836            for edge in owners {
3837                assert!(position(edge.source) < position(observable));
3838            }
3839        }
3840    }
3841
3842    #[test]
3843    fn try_new_rejects_invalid_distance() {
3844        // Illegal distances fail at the API boundary, not deep in block
3845        // compilation: an even or sub-3 distance never yields a `CompileConfig`.
3846        assert_eq!(CompileConfig::try_new(2), Err(crate::InvalidDistance(2)));
3847        assert_eq!(CompileConfig::try_new(1), Err(crate::InvalidDistance(1)));
3848        CompileConfig::try_new(3).unwrap();
3849    }
3850
3851    #[test]
3852    fn compile_preserves_scaled_cube_top_temporal_pipe_ref() {
3853        let mut graph = BlockGraph::new();
3854        graph.add_block(
3855            bloq_graph::Block::new(ivec3(0, 0, 0), BlockKind::Cube(CubeKind::ZXZ))
3856                .with_height("2d".parse().expect("valid height"))
3857                .unwrap(),
3858        );
3859        graph.add_block(bloq_graph::Block::new(
3860            ivec3(0, 0, 2),
3861            BlockKind::Cube(CubeKind::ZXZ),
3862        ));
3863        graph.add_pipe(bloq_graph::Pipe::new(
3864            ivec3(0, 0, 1),
3865            bloq_graph::Direction::ZPLUS,
3866        ));
3867        let ctx = CompileContext::new(CompileConfig::new(3));
3868
3869        let artifacts = ctx.compile(&graph).expect("scaled temporal graph compiles");
3870        let pipes = artifacts
3871            .bloq
3872            .edges()
3873            .flat_map(|edge| edge.edge.pipes().iter())
3874            .collect::<Vec<_>>();
3875
3876        assert_eq!(artifacts.bloq.quantum_node_count(), 2);
3877        assert_eq!(pipes.len(), 1);
3878        assert_eq!(pipes[0].pipe.src, ivec3(0, 0, 1));
3879        assert_eq!(pipes[0].pipe.dst, ivec3(0, 0, 2));
3880        assert_eq!(
3881            source_timeline(&artifacts.bloq, ivec3(0, 0, 0)),
3882            Some(&bloq_ir::QuantumTimeline {
3883                layer_round_ends: vec![3, 6],
3884            })
3885        );
3886    }
3887
3888    #[test]
3889    fn tall_cube_timeline_packs_from_the_connected_side() {
3890        let tall = |pos| {
3891            Block::new(pos, BlockKind::Cube(CubeKind::ZXZ))
3892                .with_height("3d/2".parse().expect("valid height"))
3893                .expect("height fits")
3894        };
3895
3896        let mut high_anchored = BlockGraph::new();
3897        high_anchored.add_block(tall(IVec3::ZERO));
3898        let high = compile(&high_anchored, 5).expect("isolated tall cube compiles");
3899        assert_eq!(
3900            source_timeline(&high, IVec3::ZERO),
3901            Some(&bloq_ir::QuantumTimeline {
3902                layer_round_ends: vec![3, 8],
3903            })
3904        );
3905
3906        let mut low_anchored = BlockGraph::new();
3907        low_anchored.add_block(Block::new(IVec3::ZERO, BlockKind::Port));
3908        low_anchored.add_block(tall(IVec3::Z));
3909        low_anchored.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZPLUS));
3910        let low = compile(&low_anchored, 5).expect("connected tall cube compiles");
3911        assert_eq!(
3912            source_timeline(&low, IVec3::Z),
3913            Some(&bloq_ir::QuantumTimeline {
3914                layer_round_ends: vec![5, 8],
3915            })
3916        );
3917    }
3918
3919    #[test]
3920    fn compile_graph_lowers_temporal_y_pair_with_inferred_basis() {
3921        let mut graph = BlockGraph::new();
3922        graph.add_block(bloq_graph::Block::new(ivec3(0, 0, 0), BlockKind::Y));
3923        graph.add_block(bloq_graph::Block::new(ivec3(0, 0, 1), BlockKind::Y));
3924        graph.add_pipe(bloq_graph::Pipe::new(
3925            ivec3(0, 0, 0),
3926            bloq_graph::Direction::ZPLUS,
3927        ));
3928        let ctx = CompileContext::new(CompileConfig::new(3));
3929
3930        let artifacts = ctx.compile(&graph).unwrap();
3931
3932        assert_eq!(artifacts.bloq.quantum_node_count(), 2);
3933    }
3934
3935    #[test]
3936    fn compile_context_outputs_only_graph_local_templates() {
3937        let unit_graph = single_cube_runtime_graph();
3938        let mut scaled_graph = BlockGraph::new();
3939        scaled_graph.add_block(
3940            bloq_graph::Block::new(ivec3(0, 0, 0), BlockKind::Cube(CubeKind::ZXZ))
3941                .with_height("2d".parse().expect("valid height"))
3942                .unwrap(),
3943        );
3944        let ctx = CompileContext::new(CompileConfig::new(3));
3945
3946        ctx.compile(&unit_graph).expect("compile unit cube");
3947        let artifacts = ctx.compile(&scaled_graph).expect("compile scaled cube");
3948
3949        assert_eq!(ctx.cache.pool_len(), 2);
3950        assert_eq!(artifacts.bloq.templates().len(), 1);
3951        let (_, node) = artifacts.bloq.nodes().next().unwrap();
3952        assert_eq!(node.expect_quantum().instances[0].template_id.0, 0);
3953    }
3954
3955    /// Gallery graphs spanning the template kinds a shared cache must handle:
3956    /// fixed cubes, a temporal Hadamard, a selective arm pair, and a T stage
3957    /// pair.
3958    const SHARED_CACHE_CASES: [GalleryItem; 4] = [
3959        GalleryItem::CNOT,
3960        GalleryItem::XMemory,
3961        GalleryItem::S,
3962        GalleryItem::T,
3963    ];
3964
3965    fn compile_to_binary(ctx: &CompileContext, graph: &BlockGraph) -> Vec<u8> {
3966        ctx.compile(graph)
3967            .expect("gallery graph compiles")
3968            .bloq
3969            .to_binary()
3970    }
3971
3972    /// Sharing a cache reorders pool ids (a context sees templates it did not
3973    /// compile) but must not reach the output: program-local template ids are
3974    /// assigned per `Bloq` in lowering order.
3975    #[test]
3976    fn shared_cache_compiles_byte_identically_to_a_private_context() {
3977        let config = CompileConfig::new(3);
3978        let cache = crate::SharedCompileCache::new();
3979
3980        for item in SHARED_CACHE_CASES {
3981            let graph = item.build().flatten().expect("gallery graph expands");
3982            let private = compile_to_binary(&CompileContext::new(config), &graph);
3983            // A second context on the same cache reads the first's templates.
3984            let warming = CompileContext::with_shared_cache(config, &cache);
3985            compile_to_binary(&warming, &graph);
3986            let shared =
3987                compile_to_binary(&CompileContext::with_shared_cache(config, &cache), &graph);
3988
3989            assert_eq!(private, shared, "{item:?} compiles identically when shared");
3990        }
3991    }
3992
3993    // ==========================================================================
3994    // Cube height in the template cache key
3995    // ==========================================================================
3996
3997    /// A cube of `height` at `(0, 0, 0)`, capped by a port on its top temporal
3998    /// endpoint. The port sits at `cells` above the anchor, so both graphs here
3999    /// present the same `ZPLUS` connectivity from different footprints.
4000    fn capped_cube(height: &str, cells: i32) -> BlockGraph {
4001        let mut graph = BlockGraph::new();
4002        graph.add_block(
4003            bloq_graph::Block::new(ivec3(0, 0, 0), BlockKind::Cube(CubeKind::ZXZ))
4004                .with_height(height.parse().expect("valid height"))
4005                .expect("cube accepts a height"),
4006        );
4007        graph.add_block(bloq_graph::Block::new(ivec3(0, 0, cells), BlockKind::Port));
4008        graph.add_pipe(bloq_graph::Pipe::new(
4009            ivec3(0, 0, cells - 1),
4010            bloq_graph::Direction::ZPLUS,
4011        ));
4012        graph
4013    }
4014
4015    /// Cubes with different graph heights but the same resolved rounds share a
4016    /// template. Reusing it must not affect graph-specific lowering.
4017    #[test]
4018    fn cube_templates_may_share_rounds_across_different_footprints() {
4019        const DISTANCE: u32 = 5;
4020        let config = CompileConfig::new(DISTANCE);
4021        let two_cell = capped_cube("2d", 2);
4022        let one_cell = capped_cube("d+5", 1);
4023        let signature_of = |graph: &BlockGraph| {
4024            let schedules =
4025                join_component_layer_schedules(graph).expect("no mixed spatial Hadamard walls");
4026            block_signatures(graph, &schedules, DISTANCE).expect("valid graph")[&ivec3(0, 0, 0)]
4027        };
4028        assert_eq!(signature_of(&two_cell), signature_of(&one_cell));
4029
4030        let private = compile_to_binary(&CompileContext::new(config), &one_cell);
4031        let cache = crate::SharedCompileCache::new();
4032        compile_to_binary(
4033            &CompileContext::with_shared_cache(config, &cache),
4034            &two_cell,
4035        );
4036        let shared = CompileContext::with_shared_cache(config, &cache);
4037        let warmed = shared.cache.pool_len();
4038
4039        assert_eq!(compile_to_binary(&shared, &one_cell), private);
4040        assert_eq!(shared.cache.pool_len(), warmed);
4041    }
4042
4043    /// Clearing detaches template shards without changing what a later
4044    /// compilation produces.
4045    #[test]
4046    fn clearing_a_shared_cache_releases_templates_and_recompiles_identically() {
4047        let config = CompileConfig::new(3);
4048        let graph = GalleryItem::CNOT
4049            .build()
4050            .flatten()
4051            .expect("gallery graph expands");
4052        let cache = crate::SharedCompileCache::new();
4053
4054        let warm = CompileContext::with_shared_cache(config, &cache);
4055        let expected = compile_to_binary(&warm, &graph);
4056        assert!(warm.cache.pool_len() > 0, "the warm shard holds templates");
4057
4058        cache.clear();
4059
4060        // New contexts start from an empty shard; `warm` retains its immutable
4061        // templates, so nothing in flight breaks.
4062        let cold = CompileContext::with_shared_cache(config, &cache);
4063        assert_eq!(cold.cache.pool_len(), 0);
4064        assert_eq!(compile_to_binary(&cold, &graph), expected);
4065    }
4066
4067    /// Contexts sharing one cache may compile concurrently; every output must
4068    /// match what a private context produces alone.
4069    #[test]
4070    fn concurrent_shared_cache_compiles_match_sequential_output() {
4071        let config = CompileConfig::new(3);
4072        let graphs: Vec<BlockGraph> = SHARED_CACHE_CASES
4073            .iter()
4074            .map(|item| item.build().flatten().expect("gallery graph expands"))
4075            .collect();
4076        let sequential: Vec<Vec<u8>> = graphs
4077            .iter()
4078            .map(|graph| compile_to_binary(&CompileContext::new(config), graph))
4079            .collect();
4080
4081        let cache = crate::SharedCompileCache::new();
4082        let concurrent: Vec<Vec<u8>> = std::thread::scope(|scope| {
4083            let handles: Vec<_> = graphs
4084                .iter()
4085                .map(|graph| {
4086                    let cache = &cache;
4087                    scope.spawn(move || {
4088                        let ctx = CompileContext::with_shared_cache(config, &cache.clone());
4089                        // Twice: the second pass races the other threads' inserts
4090                        // against warm hits on this thread.
4091                        compile_to_binary(&ctx, graph);
4092                        compile_to_binary(&ctx, graph)
4093                    })
4094                })
4095                .collect();
4096            handles
4097                .into_iter()
4098                .map(|handle| handle.join().expect("compile thread does not panic"))
4099                .collect()
4100        });
4101
4102        assert_eq!(concurrent, sequential);
4103    }
4104
4105    #[test]
4106    fn compile_graph_lowers_isolated_diagonal_walking_block() {
4107        let mut graph = BlockGraph::new();
4108        graph.add_block(bloq_graph::Block::new(
4109            ivec3(0, 0, 0),
4110            BlockKind::Walking(
4111                WalkingKind::new(WalkingBoundaryKind::ZXZ, glam::ivec2(1, 1)).unwrap(),
4112            ),
4113        ));
4114        let ctx = CompileContext::new(CompileConfig::new(3));
4115
4116        let artifacts = ctx.compile(&graph).unwrap();
4117
4118        assert_eq!(artifacts.bloq.quantum_node_count(), 1);
4119    }
4120
4121    #[test]
4122    fn compile_graph_lowers_isolated_axial_walking_block() {
4123        let mut graph = BlockGraph::new();
4124        graph.add_block(bloq_graph::Block::new(
4125            ivec3(0, 0, 0),
4126            BlockKind::Walking(
4127                WalkingKind::new(WalkingBoundaryKind::ZXZ, glam::ivec2(1, 0)).unwrap(),
4128            ),
4129        ));
4130        let ctx = CompileContext::new(CompileConfig::new(3));
4131
4132        let artifacts = ctx.compile(&graph).unwrap();
4133
4134        assert_eq!(artifacts.bloq.quantum_node_count(), 1);
4135        assert_eq!(
4136            source_timeline(&artifacts.bloq, IVec3::ZERO),
4137            Some(&bloq_ir::QuantumTimeline {
4138                layer_round_ends: vec![4, 8],
4139            })
4140        );
4141    }
4142
4143    #[test]
4144    fn compile_graph_sets_patch_rotation_timeline() {
4145        let mut graph = BlockGraph::new();
4146        let kind = PatchRotationKind::new(Basis::X, glam::IVec2::X).expect("valid rotation");
4147        let start = IVec3::Z;
4148        graph.add_block(Block::new(IVec3::ZERO, BlockKind::Port));
4149        graph.add_block(Block::new(start, BlockKind::PatchRotation(kind)));
4150        graph.add_block(Block::new(ivec3(1, 0, 3), BlockKind::Port));
4151        graph.add_pipe(Pipe::new(start, Direction::ZMINUS));
4152        graph.add_pipe(Pipe::new(kind.end_position(start), Direction::ZPLUS));
4153
4154        let program = compile(&graph, 3).expect("patch rotation compiles");
4155        assert_eq!(
4156            source_timeline(&program, start),
4157            Some(&bloq_ir::QuantumTimeline {
4158                layer_round_ends: vec![3, 6],
4159            })
4160        );
4161    }
4162
4163    #[test]
4164    fn compile_graph_lowers_ghz_slide_then_glide_gallery_asset() {
4165        let graph = GalleryItem::GHZSlideThenGlide
4166            .build()
4167            .flatten()
4168            .expect("gallery graph expands");
4169        let ctx = CompileContext::new(CompileConfig::new(3));
4170        let variants = graph
4171            .fill_ports_auto()
4172            .expect("GHZ slide-then-glide ports fill");
4173        assert!(!variants.is_empty());
4174
4175        for (variant, (filled_graph, _)) in variants.iter().enumerate() {
4176            let artifacts = ctx.compile(filled_graph).unwrap_or_else(|error| {
4177                panic!("GHZ slide-then-glide fill variant {variant} compiles: {error}")
4178            });
4179
4180            artifacts
4181                .bloq
4182                .validate()
4183                .expect("GHZ slide-then-glide template instance refs validate");
4184        }
4185    }
4186
4187    /// The point of `compile(&self)`: one context, many threads, no `&mut`
4188    /// juggling — and identical output to compiling each graph alone.
4189    #[test]
4190    fn one_context_compiles_concurrently_from_shared_borrows() {
4191        let graphs: Vec<BlockGraph> = SHARED_CACHE_CASES
4192            .iter()
4193            .map(|item| item.build().flatten().expect("gallery graph expands"))
4194            .collect();
4195        let expected: Vec<Vec<u8>> = graphs
4196            .iter()
4197            .map(|graph| compile_to_binary(&CompileContext::new(CompileConfig::default()), graph))
4198            .collect();
4199
4200        let ctx = CompileContext::new(CompileConfig::default());
4201        let concurrent: Vec<Vec<u8>> = std::thread::scope(|scope| {
4202            let handles: Vec<_> = graphs
4203                .iter()
4204                .map(|graph| {
4205                    let ctx = &ctx;
4206                    scope.spawn(move || compile_to_binary(ctx, graph))
4207                })
4208                .collect();
4209            handles
4210                .into_iter()
4211                .map(|handle| handle.join().expect("compile thread does not panic"))
4212                .collect()
4213        });
4214
4215        assert_eq!(concurrent, expected);
4216    }
4217
4218    #[test]
4219    fn one_shot_compile_matches_the_context_path() {
4220        let graph = GalleryItem::CNOT
4221            .build()
4222            .flatten()
4223            .expect("gallery graph expands");
4224        let expected = CompileContext::new(CompileConfig::new(3))
4225            .compile(&graph)
4226            .expect("context compiles")
4227            .bloq;
4228
4229        assert_eq!(
4230            compile(&graph, 3).expect("one-shot compiles").to_binary(),
4231            expected.to_binary()
4232        );
4233        assert_eq!(
4234            compile_with(&graph, CompileConfig::default())
4235                .expect("configured one-shot compiles")
4236                .bloq
4237                .to_binary(),
4238            expected.to_binary()
4239        );
4240    }
4241
4242    /// The one-shot validates its distance instead of panicking like
4243    /// `CompileConfig::new` — it is the path user input reaches.
4244    #[test]
4245    fn one_shot_compile_rejects_an_even_distance() {
4246        assert!(matches!(
4247            compile(
4248                &GalleryItem::CNOT
4249                    .build()
4250                    .flatten()
4251                    .expect("gallery graph expands"),
4252                4
4253            ),
4254            Err(CompileError::InvalidDistance(4))
4255        ));
4256    }
4257
4258    #[test]
4259    fn temporal_hadamard_from_port_uses_the_pipe_basis() {
4260        let mut graph = BlockGraph::new();
4261        graph.add_block(Block::new(IVec3::ZERO, BlockKind::Port));
4262        graph.add_block(Block::new(IVec3::Z, BlockKind::Cube(CubeKind::ZXZ)));
4263        graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZPLUS).with_hadamard());
4264
4265        compile(&graph, 3)
4266            .expect("Port -> H -> ZXZ compiles")
4267            .validate()
4268            .expect("compiled Bloq validates");
4269    }
4270
4271    #[test]
4272    fn temporal_hadamard_from_tall_cube_uses_its_top_endpoint() {
4273        let mut graph = BlockGraph::new();
4274        graph.add_block(
4275            Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ))
4276                .with_height("2d".parse().expect("valid height"))
4277                .expect("cube accepts height"),
4278        );
4279        graph.add_block(Block::new(IVec3::new(0, 0, 2), BlockKind::Port));
4280        graph.add_pipe(Pipe::new(IVec3::Z, Direction::ZPLUS).with_hadamard());
4281
4282        compile(&graph, 3)
4283            .expect("tall ZXZ -> H -> Port compiles")
4284            .validate()
4285            .expect("compiled Bloq validates");
4286    }
4287
4288    #[test]
4289    fn compiled_metadata_round_trips_through_the_program() {
4290        let bloq = compile(
4291            &GalleryItem::CNOT
4292                .build()
4293                .flatten()
4294                .expect("gallery graph expands"),
4295            5,
4296        )
4297        .expect("compiles");
4298
4299        assert_eq!(compiled_distance(&bloq), Some(5));
4300        assert_eq!(compiled_distance(&Bloq::default()), None);
4301    }
4302
4303    fn cached_module_definitions(
4304        context: &CompileContext,
4305        program: &BlockGraph,
4306    ) -> std::collections::HashMap<String, Arc<PreparedDefinitionObject>> {
4307        let orientations = program.definition_orientations();
4308        program
4309            .module_definition_cache_keys()
4310            .into_iter()
4311            .map(|(name, implementation)| {
4312                let object = context
4313                    .cache
4314                    .definition_object(&DefinitionObjectCacheKey {
4315                        implementation,
4316                        orientations: orientations[&name].clone(),
4317                    })
4318                    .expect("compiled definition is cached");
4319                (name, object)
4320            })
4321            .collect()
4322    }
4323
4324    #[test]
4325    fn shared_cache_reuses_the_compiled_module_object() {
4326        let cache = crate::SharedCompileCache::new();
4327        let config = CompileConfig::default();
4328        let first = CompileContext::with_shared_cache(config, &cache)
4329            .compile_object(&GalleryItem::PhaseGradientK4.build())
4330            .unwrap();
4331        let second = CompileContext::with_shared_cache(config, &cache)
4332            .compile_object(&GalleryItem::PhaseGradientK4.build())
4333            .unwrap();
4334
4335        assert!(Arc::ptr_eq(&first.artifacts, &second.artifacts));
4336    }
4337
4338    #[test]
4339    fn module_link_reports_a_missing_physical_variant() {
4340        let mut graph = BlockGraph::new();
4341        graph.add_block(Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ)));
4342        let definitions = [Arc::new(PreparedDefinitionObject {
4343            name: "Leaf".into(),
4344            dependencies: Vec::new(),
4345            variants: crate::FxMap::default(),
4346        })];
4347        let sites = [PreparedSiteRelocation {
4348            position: IVec3::ZERO,
4349            definition: 0,
4350            local_position: IVec3::ZERO,
4351        }];
4352
4353        assert!(matches!(
4354            select_module_link_templates_with_schedule(
4355                CompileConfig::default(),
4356                &graph,
4357                &SpatialPortExpansionMap::default(),
4358                &definitions,
4359                &sites,
4360                &crate::signature::join_component_layer_schedules(&graph).unwrap(),
4361                &crate::FxMap::default(),
4362            ),
4363            Err(CompileError::MissingModulePhysicalVariant { .. })
4364        ));
4365    }
4366
4367    #[test]
4368    fn clearing_shared_cache_evicts_objects_from_live_contexts() {
4369        let cache = crate::SharedCompileCache::new();
4370        let context = CompileContext::with_shared_cache(CompileConfig::default(), &cache);
4371        let program = GalleryItem::PhaseGradientK4.build();
4372        let first = context.compile_object(&program).unwrap();
4373
4374        cache.clear();
4375        let second = context.compile_object(&program).unwrap();
4376
4377        assert!(!Arc::ptr_eq(&first.artifacts, &second.artifacts));
4378    }
4379
4380    #[test]
4381    fn flat_oracle_artifacts_do_not_replace_hierarchical_objects() {
4382        let flat = GalleryItem::CNOT.build().flatten().unwrap();
4383        let structured = flat.clone().with_inferred_interface().unwrap();
4384        assert_eq!(flat.to_blog_text(), structured.to_blog_text());
4385        let context = CompileContext::new(CompileConfig::default());
4386        let oracle = context.compile_object(&flat).unwrap();
4387        let hierarchy = context.compile_object(&structured).unwrap();
4388        assert!(!Arc::ptr_eq(&oracle.artifacts, &hierarchy.artifacts));
4389        assert!(!cached_module_definitions(&context, &structured).is_empty());
4390    }
4391
4392    #[test]
4393    fn unified_compile_revalidates_edited_hierarchy_before_cache_reuse() {
4394        let mut graph = BlockGraph::from_text(
4395            "BLOG 1.0\nmodule Leaf {\n0: ZXZ [0,0,0]\n}\n\
4396             module main {\nchild: Leaf @ [0,0,0]\n}\n",
4397        )
4398        .unwrap();
4399        let context = CompileContext::new(CompileConfig::default());
4400        context.compile(&graph).unwrap();
4401        context
4402            .summarize(&graph, ModuleCertificationLimits::DEFAULT)
4403            .unwrap();
4404        graph.instances[0].definition = "Missing".to_string();
4405        let error = context.compile(&graph).unwrap_err();
4406        assert!(matches!(error, CompileError::Module(_)), "{error:?}");
4407        assert!(error.to_string().contains("Missing"), "{error}");
4408        let summary_error = context
4409            .summarize(&graph, ModuleCertificationLimits::DEFAULT)
4410            .unwrap_err();
4411        assert!(
4412            matches!(summary_error, ModuleCertificationError::Graph { .. }),
4413            "{summary_error:?}"
4414        );
4415        assert!(
4416            summary_error.to_string().contains("Missing"),
4417            "{summary_error}"
4418        );
4419
4420        graph.instances[0].definition = BlockGraph::ENTRY_MODULE.to_string();
4421        let summary_error = context
4422            .summarize(&graph, ModuleCertificationLimits::DEFAULT)
4423            .unwrap_err();
4424        assert!(
4425            matches!(summary_error, ModuleCertificationError::Graph { .. }),
4426            "{summary_error:?}"
4427        );
4428        assert!(
4429            summary_error.to_string().contains("cycle"),
4430            "{summary_error}"
4431        );
4432    }
4433
4434    #[test]
4435    fn unified_compile_checks_interfaces_added_to_a_plain_graph() {
4436        let mut graph = single_cube_runtime_graph();
4437        graph.interface.quantum_ports.push(bloq_graph::QuantumPort {
4438            name: "missing_port".to_string(),
4439            position: IVec3::X,
4440            direction: bloq_graph::PortDirection::Input,
4441            resource_type: "data".to_string(),
4442        });
4443        assert!(graph.has_module_structure());
4444        let error = CompileContext::new(CompileConfig::default())
4445            .compile(&graph)
4446            .unwrap_err();
4447        assert!(matches!(error, CompileError::Module(_)), "{error:?}");
4448        assert!(error.to_string().contains("missing_port"), "{error}");
4449    }
4450
4451    #[test]
4452    fn module_assembly_retains_cache_hits_across_a_clear() {
4453        let program = BlockGraph::from_text(
4454            "BLOG 1.0\nmodule Leaf {\n0: ZXZ [0,0,0]\n}\n\
4455             module main {\nleaf: Leaf @ [0,0,0]\n}\n",
4456        )
4457        .unwrap();
4458        let cache = crate::SharedCompileCache::new();
4459        let context = CompileContext::with_shared_cache(CompileConfig::default(), &cache);
4460        context.compile_object(&program).unwrap();
4461        let mut retained = cached_module_definitions(&context, &program);
4462        let root = retained.remove(BlockGraph::ENTRY_MODULE).unwrap();
4463        let parts = std::collections::HashMap::from([(root.name.clone(), root.variants.clone())]);
4464        let mut names = program
4465            .module_definition_cache_keys()
4466            .into_keys()
4467            .collect::<Vec<_>>();
4468        names.sort_unstable();
4469
4470        // A deterministic interleaving: eviction after lookups but before
4471        // assembling a new root from a cached child and newly compiled parts.
4472        cache.clear();
4473        let definitions = build_definition_objects(
4474            &context.cache,
4475            &names,
4476            parts,
4477            retained,
4478            &program.module_definition_cache_keys(),
4479            &program.definition_orientations(),
4480            &program,
4481        );
4482
4483        let rebuilt_root = definitions
4484            .iter()
4485            .find(|definition| definition.name == "main")
4486            .unwrap();
4487        assert!(Arc::ptr_eq(
4488            &rebuilt_root.dependencies[0],
4489            &root.dependencies[0]
4490        ));
4491    }
4492
4493    #[test]
4494    fn serial_and_parallel_module_builds_link_identically() {
4495        let continuing = BlockGraph::from_text(
4496            "BLOG 1.0
4497module main {
4498  0: ZXZ [0,0,0]
4499  1: ZXZ [0,0,2]
4500  9: ZXZ [2,0,0]
4501  branch b {
4502    false {
4503      2: XZX [0,0,1]
4504      0 -H> +Z
4505      [0,0,1] -H> +Z
4506    }
4507    true {
4508      3: ZXZ [0,0,1]
4509      0 -> +Z
4510      [0,0,1] -> +Z
4511    }
4512  }
4513  m = measure 9
4514  resolve b if m
4515}
4516",
4517        )
4518        .unwrap();
4519        for (label, program) in [
4520            ("phase gradient", GalleryItem::PhaseGradientK4.build()),
4521            ("CCZ", GalleryItem::CCZGateTeleport.build()),
4522            ("continuing", continuing),
4523        ] {
4524            let serial_context = CompileContext::new(CompileConfig::default());
4525            let serial = serial_context
4526                .compile_object_with_jobs(&program, NonZeroUsize::MIN)
4527                .and_then(|object| serial_context.link_object(&object))
4528                .unwrap();
4529            let parallel_context = CompileContext::new(CompileConfig::default());
4530            let parallel = parallel_context
4531                .compile_object_with_jobs(&program, NonZeroUsize::new(4).expect("four is nonzero"))
4532                .and_then(|object| parallel_context.link_object(&object))
4533                .unwrap();
4534
4535            assert_eq!(
4536                serial.bloq.to_binary(),
4537                parallel.bloq.to_binary(),
4538                "{label}"
4539            );
4540        }
4541    }
4542
4543    #[test]
4544    fn definition_objects_survive_a_different_root_archive_key() {
4545        let cache = crate::SharedCompileCache::new();
4546        let config = CompileConfig::default();
4547        let program = BlockGraph::from_text(
4548            "BLOG 1.0\nmodule Leaf {\n0: ZXZ [0,0,0]\n}\n\
4549             module main {\na: Leaf @ [0,0,0]\nb: Leaf @ [2,0,0]\n}\n",
4550        )
4551        .unwrap();
4552        let first_context = CompileContext::with_shared_cache(config, &cache);
4553        let first = first_context.compile_object(&program).unwrap();
4554        let first_definitions = cached_module_definitions(&first_context, &program);
4555
4556        let mut modules = program
4557            .modules()
4558            .map(BlockGraph::clone_local_definition)
4559            .collect::<Vec<_>>();
4560        let mut unused = BlockGraph::from_text(include_str!(
4561            "../../docs/fixtures/conditional_cz_strip.blog"
4562        ))
4563        .unwrap()
4564        .clone_local_definition();
4565        unused.name = "Unused".into();
4566        modules.push(unused);
4567        let with_unused = bloq_graph::BlockGraph::from_definitions(modules)
4568            .expect("unused definition keeps the root valid");
4569        let second_context = CompileContext::with_shared_cache(config, &cache);
4570        let second = second_context.compile_object(&with_unused).unwrap();
4571        let second_definitions = cached_module_definitions(&second_context, &with_unused);
4572
4573        assert!(!Arc::ptr_eq(&first.artifacts, &second.artifacts));
4574        assert_eq!(first_definitions.len(), second_definitions.len());
4575        for (name, left) in first_definitions {
4576            assert!(
4577                Arc::ptr_eq(&left, &second_definitions[&name]),
4578                "{name} was rebuilt"
4579            );
4580        }
4581    }
4582
4583    #[test]
4584    fn spatial_module_seam_promotes_both_sides_before_template_selection() {
4585        let program = BlockGraph::from_text(
4586            "BLOG 1.0\n\n\
4587             module A {\n\
4588               out q: data = 1\n\
4589               0: XXZ [0,0,0]\n\
4590               1: Port [1,0,0] role=output <q>\n\
4591               0 -> +X\n\
4592             }\n\n\
4593             module B {\n\
4594               in q: data = 0\n\
4595               0: Port [-1,0,0] role=input <q>\n\
4596               1: ZXZ [0,0,0]\n\
4597               0 -> +X\n\
4598             }\n\n\
4599             module main {\n\
4600               a: A @ [0,0,0]\n\
4601               b: B @ [1,0,0]\n\
4602               a.q -> b.q\n\
4603             }\n",
4604        )
4605        .unwrap();
4606        let context = CompileContext::new(CompileConfig::default());
4607        let linked = context.compile(&program).unwrap();
4608        let graph = program.flatten().unwrap().fix_shadowed_faces();
4609        let signatures = select_link_signatures(
4610            context.config,
4611            &graph,
4612            &SpatialPortExpansionMap::default(),
4613            &context.flat_signature_builds,
4614        )
4615        .unwrap();
4616
4617        for pos in [IVec3::ZERO, IVec3::X] {
4618            let signature = &signatures[&pos];
4619            assert_eq!(signature.layer_schedule, Some(LayerSchedule::Padded));
4620        }
4621        linked.bloq.validate().unwrap();
4622        let component = linked
4623            .bloq
4624            .nodes()
4625            .find_map(|(_, node)| (node.block_members().len() == 2).then_some(node))
4626            .expect("the spatial seam fuses both module cubes");
4627        assert_eq!(component.expect_quantum().instances.len(), 2);
4628    }
4629
4630    #[test]
4631    fn fat_definition_object_reuses_a_cube_across_compact_and_padded_seams() {
4632        let pair = |neighbor: &str| {
4633            BlockGraph::from_text(&format!(
4634                "BLOG 1.0\n\n\
4635                 module A {{\n\
4636                   out q: data = 1\n\
4637                   0: ZXZ [0,0,0]\n\
4638                   1: Port [1,0,0] role=output <q>\n\
4639                   0 -> +X\n\
4640                 }}\n\n\
4641                 module B {{\n\
4642                   in q: data = 0\n\
4643                   0: Port [-1,0,0] role=input <q>\n\
4644                   1: {neighbor} [0,0,0]\n\
4645                   0 -> +X\n\
4646                 }}\n\n\
4647                 module main {{\n\
4648                   a: A @ [0,0,0]\n\
4649                   b: B @ [1,0,0]\n\
4650                   a.q -> b.q\n\
4651                 }}\n"
4652            ))
4653            .unwrap()
4654        };
4655        let cache = crate::SharedCompileCache::new();
4656        let config = CompileConfig::default();
4657        let compact_program = pair("ZXZ");
4658        let padded_program = pair("XXZ");
4659        let compact_context = CompileContext::with_shared_cache(config, &cache);
4660        let compact = compact_context.compile(&compact_program).unwrap();
4661        let compact_definition = cached_module_definitions(&compact_context, &compact_program)
4662            .remove("A")
4663            .unwrap();
4664        let padded_context = CompileContext::with_shared_cache(config, &cache);
4665        let padded = padded_context.compile(&padded_program).unwrap();
4666        let padded_definition = cached_module_definitions(&padded_context, &padded_program)
4667            .remove("A")
4668            .unwrap();
4669
4670        assert!(Arc::ptr_eq(&compact_definition, &padded_definition));
4671        let schedules = compact_definition
4672            .variants
4673            .keys()
4674            .filter(|variant| variant.local_position == IVec3::ZERO)
4675            .filter_map(|variant| variant.signature.layer_schedule)
4676            .collect::<crate::FxSet<_>>();
4677        assert!(schedules.contains(&LayerSchedule::Compact));
4678        assert!(schedules.contains(&LayerSchedule::Padded));
4679        let selected = |program: &BlockGraph| {
4680            let context = CompileContext::new(config);
4681            let graph = program.flatten().unwrap().fix_shadowed_faces();
4682            select_link_signatures(
4683                context.config,
4684                &graph,
4685                &SpatialPortExpansionMap::default(),
4686                &context.flat_signature_builds,
4687            )
4688            .unwrap()[&IVec3::ZERO]
4689                .layer_schedule
4690                .unwrap()
4691        };
4692        assert_eq!(selected(&compact_program), LayerSchedule::Compact);
4693        assert_eq!(selected(&padded_program), LayerSchedule::Padded);
4694        compact.bloq.validate().unwrap();
4695        padded.bloq.validate().unwrap();
4696    }
4697
4698    #[test]
4699    fn flat_signature_guard_observes_worker_threads() {
4700        let builds = std::sync::atomic::AtomicUsize::new(0);
4701        let graph = GalleryItem::CNOT
4702            .build()
4703            .flatten()
4704            .expect("gallery graph expands");
4705        std::thread::scope(|scope| {
4706            scope
4707                .spawn(|| {
4708                    select_link_signatures(
4709                        CompileConfig::default(),
4710                        &graph,
4711                        &SpatialPortExpansionMap::default(),
4712                        &builds,
4713                    )
4714                    .unwrap();
4715                })
4716                .join()
4717                .unwrap();
4718        });
4719
4720        assert_eq!(builds.load(std::sync::atomic::Ordering::Relaxed), 1);
4721    }
4722
4723    #[test]
4724    fn module_link_never_discovers_flat_signatures() {
4725        let context = CompileContext::new(CompileConfig::default());
4726        context
4727            .compile(&GalleryItem::PhaseGradientK4.build())
4728            .unwrap();
4729        let branch = BlockGraph::from_definitions(vec![BlockGraph::definition(
4730            BlockGraph::ENTRY_MODULE,
4731            structural_branch_graph(),
4732            bloq_graph::ModuleInterface::default(),
4733            Vec::new(),
4734            Vec::new(),
4735            Vec::new(),
4736        )])
4737        .unwrap();
4738        context.compile(&branch).unwrap();
4739
4740        assert_eq!(
4741            context
4742                .flat_signature_builds
4743                .load(std::sync::atomic::Ordering::Relaxed),
4744            0
4745        );
4746    }
4747
4748    /// A spatial Hadamard wall compiles, but its effective distance may fall
4749    /// below `d` — the artifact carries that advisory so a front end cannot
4750    /// forget to ask for it (LIM-016).
4751    #[test]
4752    fn artifacts_carry_the_spatial_hadamard_distance_warning() {
4753        let mut graph = BlockGraph::new();
4754        graph.add_block(Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::XZX)));
4755        graph.add_block(Block::new(IVec3::X, BlockKind::Cube(CubeKind::XXZ)));
4756        graph.add_pipe(
4757            bloq_graph::Pipe::new(IVec3::ZERO, bloq_graph::Direction::XPLUS).with_hadamard(),
4758        );
4759
4760        let artifacts = compile_with(&graph, CompileConfig::default()).expect("wall compiles");
4761
4762        assert_eq!(
4763            artifacts.warnings,
4764            crate::spatial_hadamard_distance_warning(&graph)
4765                .into_iter()
4766                .collect::<Vec<_>>()
4767        );
4768        assert!(!artifacts.warnings.is_empty());
4769        assert!(
4770            compile_with(
4771                &GalleryItem::CNOT
4772                    .build()
4773                    .flatten()
4774                    .expect("gallery graph expands"),
4775                CompileConfig::default()
4776            )
4777            .expect("CNOT compiles")
4778            .warnings
4779            .is_empty()
4780        );
4781    }
4782}