Skip to main content

bloq_compile/
error.rs

1use glam::{IVec2, IVec3};
2use thiserror::Error;
3
4/// An error raised while validating or compiling a [`BlockGraph`](bloq_graph::BlockGraph).
5///
6/// Variants cover invalid configuration, unsupported or malformed blocks,
7/// observable/detector composition failures, and errors forwarded from the
8/// graph, circuit, and IR layers.
9#[derive(Debug, Error)]
10#[non_exhaustive]
11pub enum CompileError {
12    /// The caller cancelled compilation at a safe checkpoint.
13    #[error("{0}")]
14    Cancelled(#[from] bloq_graph::ComputationCancelled),
15    /// Compiler-configured Boolean analysis exceeded a resource limit.
16    #[error(
17        "{0}; {help}",
18        help = bloq_graph::ModuleCertificationLimits::RESOURCE_LIMIT_HELP
19    )]
20    BooleanResource(#[from] bloq_utils::boolean::BooleanResourceError),
21    /// The requested code distance is outside the supported odd range.
22    #[error("invalid distance {0}: must be odd and in 3..=255")]
23    InvalidDistance(u32),
24    /// A compiled object was linked under a different compile configuration.
25    #[error("compiled object target {object:?} does not match linker target {linker:?}")]
26    CompiledObjectTargetMismatch {
27        /// Configuration used to build the object.
28        object: Box<crate::CompileConfig>,
29        /// Configuration used by the linker.
30        linker: Box<crate::CompileConfig>,
31    },
32    /// A linked module site has no compiled template matching its physical signature.
33    #[error(
34        "module object '{module}' has no physical ABI variant for local block {local_position:?} linked at {linked_position:?}: {signature}"
35    )]
36    MissingModulePhysicalVariant {
37        /// Module definition containing the site.
38        module: String,
39        /// Site position within the definition.
40        local_position: IVec3,
41        /// Site position after module linking.
42        linked_position: IVec3,
43        /// Missing physical signature.
44        signature: String,
45    },
46    /// A Clifford proxy received the wrong number of selective-block pins.
47    #[error("Clifford proxy requires {expected} selective pins, got {actual}")]
48    CliffordProxyPinCount {
49        /// Required number of pins.
50        expected: usize,
51        /// Supplied number of pins.
52        actual: usize,
53    },
54    /// The two per-basis arms must present the same output signature.
55    #[error("Selective({kind}) arm signature mismatch: {reason}")]
56    SelectiveArmSignatureMismatch {
57        /// Selective block whose arms disagree.
58        kind: bloq_graph::SelectiveKind,
59        /// Signature difference found between the arms.
60        reason: String,
61    },
62    /// A block's connected faces are unsupported by fixed-bulk lowering.
63    #[error("{kind} block has invalid fixed-bulk connectivity {connectivity:?}")]
64    InvalidConnectivity {
65        /// Block kind being validated.
66        kind: bloq_graph::BlockKind,
67        /// Unsupported face connectivity.
68        connectivity: crate::signature::Connectivity,
69    },
70    /// A temporal leaf block has no neighboring boundary basis.
71    #[error("{kind} block requires a temporal cube neighbor to determine its boundary basis")]
72    MissingBoundaryBasis {
73        /// Block kind requiring the basis.
74        kind: bloq_graph::BlockKind,
75    },
76    /// A spatial port is connected to a block that cannot supply cube geometry.
77    #[error("spatial Port at {port:?} is connected to non-cube block at {neighbor:?}")]
78    SpatialPortNeighborNotCube {
79        /// Spatial port position.
80        port: IVec3,
81        /// Connected non-cube position.
82        neighbor: IVec3,
83    },
84    /// A spatial port's virtual cube kind could not be inferred.
85    #[error("cannot infer the virtual cube kind for spatial Port at {port:?}")]
86    SpatialPortCubeInferenceFailed {
87        /// Spatial port position.
88        port: IVec3,
89    },
90    /// A spatial port borders a cube taller than the substitution supports.
91    #[error(
92        "spatial Port at {port:?} borders a tall cube (height={height}); tall spatial-Port substitution is not supported yet"
93    )]
94    TallSpatialPortUnsupported {
95        /// Spatial port position.
96        port: IVec3,
97        /// Unsupported neighboring cube height.
98        height: bloq_graph::CubeHeight,
99    },
100    /// No unused coordinate is available for a multiplexed logical output.
101    #[error("cannot allocate an external physical qubit for Multiplex Port at {port:?}")]
102    MultiplexOutputCoordinateUnavailable {
103        /// Multiplex port position.
104        port: IVec3,
105    },
106    /// A spatial Hadamard wall endpoint is not a cube.
107    #[error(
108        "spatial Hadamard pipe endpoint {pos:?} is not a cube; the wall's extended stabilizers \
109         reach into both neighbours' seam-adjacent data columns, which only a cube patch has"
110    )]
111    SpatialHadamardUnsupportedEndpoint {
112        /// Unsupported endpoint position.
113        pos: IVec3,
114    },
115    /// One spatial component contains Hadamard walls on both spatial axes.
116    #[error(
117        "spatial component at {pos:?} mixes X- and Y-axis Hadamard walls; fixed-bulk compilation \
118         supports one spatial Hadamard axis per connected component"
119    )]
120    MixedSpatialHadamardUnsupported {
121        /// Position identifying the mixed-axis component.
122        pos: IVec3,
123    },
124    /// A cube height resolves to fewer than two syndrome rounds.
125    #[error(
126        "cube at {pos:?} with height={height} compiles to {rounds} syndrome rounds at distance \
127         {distance}; a cube needs at least 2 (one initialization stage, one measurement stage)"
128    )]
129    CubeHeightTooShort {
130        /// Cube position.
131        pos: IVec3,
132        /// Authored symbolic height.
133        height: bloq_graph::CubeHeight,
134        /// Code distance used to resolve the height.
135        distance: u32,
136        /// Resolved syndrome-round count.
137        rounds: i64,
138    },
139    /// A block position cannot be mapped into the signed physical coordinate range.
140    #[error(
141        "block coordinate {block_xy:?} at distance {distance} exceeds the signed 32-bit qubit layout"
142    )]
143    BlockLayoutCoordinateOverflow {
144        /// Block's spatial coordinate.
145        block_xy: glam::IVec2,
146        /// Code distance scaling the coordinate.
147        distance: u32,
148    },
149    /// Two same-layer nodes own the same physical qubit.
150    #[error(
151        "WF-LAYOUT: same-layer Bloq nodes {first:?} and {second:?} own overlapping physical qubit {qubit:?}"
152    )]
153    OverlappingNodeQubit {
154        /// First conflicting node.
155        first: bloq_ir::BloqNodeId,
156        /// Second conflicting node.
157        second: bloq_ir::BloqNodeId,
158        /// Shared physical coordinate.
159        qubit: IVec2,
160    },
161    /// Patch-rotation circuit construction failed.
162    #[error("PatchRotation block construction failed: {reason}")]
163    PatchRotationConstructionFailed {
164        /// Construction failure detail.
165        reason: String,
166    },
167    /// Stabilizer-flow composition failed for one physical node.
168    #[error("program node flow composition failed for layer {layer} members {members:?}: {source}")]
169    NodeFlowCompositionFailed {
170        /// Physical layer containing the node.
171        layer: i64,
172        /// Source blocks merged into the node.
173        members: Vec<IVec3>,
174        /// Underlying flow-composition failure.
175        #[source]
176        source: bloq_circuit::FlowError,
177    },
178    /// Program detector-flow composition failed.
179    #[error("program detector composition failed: {source}")]
180    DetectorFlowCompositionFailed {
181        /// Underlying flow-composition failure.
182        #[source]
183        source: bloq_circuit::FlowError,
184    },
185    /// A repeat boundary contains an unsupported moving residual operator.
186    #[error(
187        "loop-boundary residual with a moving operator (non-empty flow start) inside a repeat body is unsupported"
188    )]
189    LoopBoundaryMovingOperatorUnsupported,
190    /// A generated conditional Pauli cannot use the supported feedback paths.
191    #[error(
192        "compiler-generated ConditionalPauli requires the commuting-feedback path or parity folding"
193    )]
194    ConditionalPauliFeedbackUnsupported,
195    /// A T block has no free neighboring cell for its cultivation spill.
196    #[error(
197        "T block at {pos:?} needs a free spatial neighbor cell (-y/+x/+y/-x) for the Steane cultivation spill; all four are occupied or reserved by another T block"
198    )]
199    TBlockNeedsFreeNeighbor {
200        /// T-block position.
201        pos: IVec3,
202    },
203    /// Walking-block circuit construction failed.
204    #[error("Walking block construction failed: {0}")]
205    WalkingConstruction(#[source] WalkError),
206    /// Observable lowering found no gateway entry for a local stabilizer.
207    #[error(
208        "observable composition failed: missing gateway entry {local_stabilizer} at {block_pos:?} for observable {observable_index}"
209    )]
210    ObservableMissingGatewayEntry {
211        /// Source block position.
212        block_pos: IVec3,
213        /// Observable being lowered.
214        observable_index: u32,
215        /// Diagnostic description of the missing local gateway key.
216        local_stabilizer: String,
217    },
218    /// A logical boundary operator disagrees across a temporal seam.
219    #[error(
220        "observable {observable_index} boundary operator is discontinuous across the temporal seam {lower:?} -> {upper:?}: the lower block's +Z `operator_out` must equal the upper block's -Z `operator_in`"
221    )]
222    ObservableSeamMismatch {
223        /// Observable being lowered.
224        observable_index: u32,
225        /// Lower seam endpoint.
226        lower: IVec3,
227        /// Upper seam endpoint.
228        upper: IVec3,
229    },
230    /// Continuing-branch assembly cannot be scheduled.
231    #[error("continuing branch assembly cannot be scheduled: {reason}")]
232    BranchAssembly {
233        /// Scheduling failure detail.
234        reason: String,
235    },
236    /// A Clifford proxy was requested for a graph with structural branches.
237    #[error(
238        "Clifford proxies do not support structural branches because their pins only resolve selective blocks"
239    )]
240    StructuralBranchCliffordProxyUnsupported,
241    /// Memory padding was requested with zero rounds.
242    #[error("memory-round padding requires at least one round (U11)")]
243    PaddingRoundsZero,
244    /// An IR seam edit failed.
245    #[error("seam edit failed (U12): {0}")]
246    Edit(#[from] bloq_ir::EditError),
247    /// The generated Bloq program failed validation.
248    #[error("{0}")]
249    BloqValidation(#[from] bloq_ir::BloqValidationError),
250    /// Physical coordinate arithmetic overflowed.
251    #[error("{0}")]
252    CoordinateOverflow(#[from] bloq_ir::CoordinateOverflowError),
253    /// Rebasing a logical boundary exceeded the signed coordinate range.
254    #[error(
255        "rebasing logical-boundary coordinate {coordinate:?} around patch offset {offset:?} exceeds the signed 32-bit lattice"
256    )]
257    LogicalBoundaryRebaseOverflow {
258        /// Boundary coordinate before rebasing.
259        coordinate: glam::IVec2,
260        /// Physical patch offset.
261        offset: glam::IVec2,
262    },
263    /// Circuit construction failed.
264    #[error("{0}")]
265    Circuit(#[from] bloq_circuit::CircuitError),
266    /// Runtime stabilizer-basis construction failed.
267    #[error("{0}")]
268    RuntimeBasis(#[from] bloq_graph::RuntimeBasisError),
269    /// Symbolic stabilizer-basis construction failed.
270    #[error("{0}")]
271    SymbolicBasis(#[from] bloq_graph::SymbolicBasisError),
272    /// Logical output-correction construction failed.
273    #[error("{0}")]
274    OutputCorrection(#[from] bloq_graph::OutputCorrectionError),
275    /// Source block-graph validation or analysis failed.
276    #[error("{0}")]
277    BlockGraph(#[from] bloq_graph::BlockGraphError),
278    /// Module certification failed.
279    #[error("{0}")]
280    ModuleCertification(#[from] bloq_graph::ModuleCertificationError),
281    /// Hierarchical block-graph validation or linking failed.
282    #[error("{0}")]
283    Module(#[from] Box<bloq_graph::ModuleError>),
284}
285
286impl From<bloq_graph::ModuleError> for CompileError {
287    fn from(error: bloq_graph::ModuleError) -> Self {
288        Self::Module(Box::new(error))
289    }
290}
291
292/// Reject `observed` units of `resource` against `limit`.
293///
294/// Every compiler budget funnels here so a breach stays a typed
295/// [`BooleanResourceError`](bloq_utils::boolean::BooleanResourceError) and is
296/// never reported as an invalid source or an unreachable choice. Callers check
297/// the would-be total *before* allocating for it, so a site growing a
298/// collection by one passes `len + 1`.
299///
300/// This only rejects; it never charges. Whether a breach also spends budget is
301/// the caller's ordering decision — charge before checking to retain the spend
302/// across a retry, check first to leave it unspent.
303pub(crate) fn check_resource(
304    resource: &'static str,
305    observed: usize,
306    limit: usize,
307) -> Result<(), CompileError> {
308    if observed > limit {
309        return Err(bloq_utils::boolean::BooleanResourceError {
310            resource,
311            observed,
312            limit,
313        }
314        .into());
315    }
316    Ok(())
317}
318
319/// Check a growing resource count without letting an unlimited cap hide overflow.
320pub(crate) fn add_resource(
321    resource: &'static str,
322    current: usize,
323    additional: usize,
324    limit: usize,
325) -> Result<usize, CompileError> {
326    let total = current.checked_add(additional).ok_or_else(|| {
327        CompileError::BooleanResource(bloq_utils::boolean::BooleanResourceError {
328            resource,
329            observed: usize::MAX,
330            limit,
331        })
332    })?;
333    check_resource(resource, total, limit)?;
334    Ok(total)
335}
336
337impl CompileError {
338    /// The verification audit inherits compiler Boolean limits, while its
339    /// depth and flattening limits retain their separate contracts.
340    pub(crate) fn from_verification_audit(error: bloq_ir::BloqValidationError) -> Self {
341        use bloq_ir::BloqValidationError;
342        use bloq_ir::lowering::NodeTemplateInstanceMergeError;
343
344        match error {
345            BloqValidationError::BooleanResource(resource)
346            | BloqValidationError::InvalidInstanceMergeStructure {
347                source: NodeTemplateInstanceMergeError::BooleanResource(resource),
348                ..
349            }
350            | BloqValidationError::InvalidTemplateCircuit {
351                source: NodeTemplateInstanceMergeError::BooleanResource(resource),
352                ..
353            } => Self::BooleanResource(resource),
354            error => Self::BloqValidation(error),
355        }
356    }
357
358    /// Whether computation stopped at a work or allocation budget, rather
359    /// than proving the source or resulting IR invalid.
360    pub fn is_resource_limited(&self) -> bool {
361        self.resource_limited_error().is_some()
362    }
363
364    /// Whether the caller cancelled this computation, including a forwarded
365    /// graph-analysis failure.
366    pub fn is_cancelled(&self) -> bool {
367        let mut error: &(dyn std::error::Error + 'static) = self;
368        loop {
369            if error.is::<bloq_graph::ComputationCancelled>() {
370                return true;
371            }
372            let Some(source) = error.source() else {
373                return false;
374            };
375            error = source;
376        }
377    }
378
379    /// Configuration help when a compilation-certification limit was exceeded.
380    ///
381    /// Standalone IR validation and circuit-flattening limits have
382    /// separate configuration contracts and return no certification hint here.
383    pub fn resource_limit_help(&self) -> Option<&'static str> {
384        let error = self.resource_limited_error()?;
385        (error.is::<bloq_utils::boolean::BooleanResourceError>()
386            || error.is::<bloq_graph::StabilizerError>()
387            || error.is::<bloq_graph::ModuleCertificationError>())
388        .then_some(bloq_graph::ModuleCertificationLimits::RESOURCE_LIMIT_HELP)
389    }
390
391    fn resource_limited_error(&self) -> Option<&(dyn std::error::Error + 'static)> {
392        let mut error: &(dyn std::error::Error + 'static) = self;
393        loop {
394            if error.is::<bloq_utils::boolean::BooleanResourceError>()
395                || matches!(
396                    error.downcast_ref::<bloq_graph::StabilizerError>(),
397                    Some(bloq_graph::StabilizerError::ResourceLimited { .. })
398                )
399                || matches!(
400                    error.downcast_ref::<bloq_graph::ModuleCertificationError>(),
401                    Some(bloq_graph::ModuleCertificationError::ResourceLimited { .. })
402                )
403                || error
404                    .downcast_ref::<bloq_ir::BloqValidationError>()
405                    .is_some_and(bloq_ir::BloqValidationError::is_resource_limited)
406                || error
407                    .downcast_ref::<bloq_ir::lowering::NodeTemplateInstanceMergeError>()
408                    .is_some_and(
409                        bloq_ir::lowering::NodeTemplateInstanceMergeError::is_resource_limited,
410                    )
411                || matches!(
412                    error.downcast_ref::<bloq_circuit::CircuitError>(),
413                    Some(bloq_circuit::CircuitError::FlattenResourceLimit { .. })
414                )
415            {
416                return Some(error);
417            }
418            error = error.source()?;
419        }
420    }
421}
422
423/// A surface code distance was rejected: distances must be odd and in
424/// `3..=255`, limiting the quadratic grids materialized during compilation.
425///
426/// Returned by [`CompileConfig::try_new`](crate::CompileConfig::try_new) so an
427/// illegal distance fails at the API boundary rather than deep in block
428/// compilation.
429#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
430#[error("invalid code distance {0}: must be odd and in 3..=255")]
431pub struct InvalidDistance(pub u32);
432
433impl From<InvalidDistance> for CompileError {
434    fn from(err: InvalidDistance) -> Self {
435        CompileError::InvalidDistance(err.0)
436    }
437}
438
439/// Why a Walking block's step construction failed, one variant per failure
440/// class of the step walk (see `block/walk.rs`).
441#[derive(Debug, Clone, Error)]
442#[non_exhaustive]
443pub enum WalkError {
444    /// A step reintroduces a qubit outside its active frame.
445    #[error("reinclude qubit {qubit} is outside the step frame")]
446    ReincludeOutsideFrame {
447        /// Out-of-frame qubit.
448        qubit: glam::IVec2,
449    },
450    /// More than one region reintroduces the same qubit.
451    #[error("multiple regions reinclude qubit {qubit}")]
452    DuplicateReinclude {
453        /// Duplicated qubit.
454        qubit: glam::IVec2,
455    },
456    /// A contracting region leaves an unmeasured qubit active.
457    #[error("contracting {basis} region kept unmeasured qubit {qubit}")]
458    ContractingKeptUnmeasuredQubit {
459        /// Contracting Pauli basis.
460        basis: bloq_graph::Basis,
461        /// Qubit left unmeasured.
462        qubit: glam::IVec2,
463    },
464    /// An expanding region reaches more than one output measurement.
465    #[error("expanding {basis} region hit multiple output measure qubits: {first} and {second}")]
466    ExpandingMultipleOutputMeasures {
467        /// Expanding Pauli basis.
468        basis: bloq_graph::Basis,
469        /// First output measurement.
470        first: glam::IVec2,
471        /// Second output measurement.
472        second: glam::IVec2,
473    },
474    /// A final measurement leaves a continuing output operator.
475    #[error("final {basis} measurement left continuing output {output}")]
476    FinalMeasurementLeftOutput {
477        /// Final measurement basis.
478        basis: bloq_graph::Basis,
479        /// Remaining output operator.
480        output: bloq_circuit::PauliMap,
481    },
482    /// A stabilizer flow references a qubit that was not measured.
483    #[error("flow references unmeasured qubit {qubit}")]
484    FlowReferencesUnmeasuredQubit {
485        /// Referenced qubit.
486        qubit: glam::IVec2,
487    },
488    /// A final flow contains a Pauli outside its required basis.
489    #[error("final {basis} flow contains non-matching Pauli {pauli} at {coord}")]
490    FinalFlowMismatchedPauli {
491        /// Required final-flow basis.
492        basis: bloq_graph::Basis,
493        /// Mismatched Pauli.
494        pauli: bloq_graph::Pauli,
495        /// Coordinate carrying the Pauli.
496        coord: glam::IVec2,
497    },
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    #[test]
505    fn flow_composition_errors_retain_their_cause() {
506        let source =
507            bloq_circuit::FlowError::CoordinateOverflow(bloq_circuit::CoordinateOverflowError {
508                coordinate: IVec2::MAX,
509                offset: IVec2::ONE,
510            });
511        for error in [
512            CompileError::NodeFlowCompositionFailed {
513                layer: 1,
514                members: vec![],
515                source: source.clone(),
516            },
517            CompileError::DetectorFlowCompositionFailed {
518                source: source.clone(),
519            },
520        ] {
521            assert_eq!(
522                std::error::Error::source(&error)
523                    .and_then(|error| error.downcast_ref::<bloq_circuit::FlowError>()),
524                Some(&source),
525            );
526        }
527    }
528}