Skip to main content

ket/
error.rs

1// SPDX-FileCopyrightText: 2026 Evandro Chagas Ribeiro da Rosa <evandro@quantuloop.com>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Error types for the Libket quantum computing library.
6//!
7//! All fallible operations in Libket return a [`Result<T>`], where the error
8//! variant is a [`KetError`]. Each variant carries a human-readable description
9//! of the failure condition that is suitable for end-user display.
10//!
11//! The enum is `#[repr(i32)]` so that it can be transmitted across FFI
12//! boundaries as a plain integer code. Use [`KetError::error_code`] to obtain
13//! the discriminant value and [`KetError::from_error_code`] to reconstruct the
14//! variant on the receiving side.
15
16/// The exhaustive set of errors that Libket operations can produce.
17///
18/// Every variant has a stable, non-zero integer discriminant (with the sole
19/// exception of [`KetError::Success`], which is always `0`) so that the enum
20/// can be used as a C-compatible status code.
21#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
22#[repr(i32)]
23pub enum KetError {
24    /// No error occurred. This variant is only used as a sentinel value in FFI
25    /// contexts where an integer `0` signals success.
26    #[error("Operation completed successfully.")]
27    Success = 0,
28
29    /// A qubit was specified as a control qubit more than once in the same gate
30    /// instruction. Each physical qubit may only appear once in the control set.
31    ///
32    /// Returned by [`crate::ir::gate::GateInstruction::control`] (and transitively
33    /// by [`crate::ir::block::BasicBlock::control`]) when
34    /// `control_qubits` contains a duplicate entry or overlaps with the existing
35    /// control set of the instruction.
36    ///
37    /// # Example
38    /// Calling `gate.control(&[q0, q0])` would trigger this error.
39    #[error(
40        "Duplicate control qubit: the same qubit index appears more than once in the \
41         control set of a single gate instruction. Each qubit may be listed at most once."
42    )]
43    DuplicateControlQubit,
44
45    /// A qubit was used as both a control and the target of the same gate
46    /// instruction, which is physically undefined.
47    ///
48    /// Returned by [`crate::ir::gate::GateInstruction::control`] (and transitively
49    /// by [`crate::ir::block::BasicBlock::control`]) when
50    /// one of the provided `control_qubits` equals `self.target`.
51    ///
52    /// # Example
53    /// Calling `gate.control(&[gate.target])` would trigger this error.
54    #[error(
55        "Control-target qubit conflict: a qubit index appears in both the control set \
56         and as the target of the same gate. A qubit cannot control its own gate."
57    )]
58    ControlTargetConflict,
59
60    /// An attempt was made to add control qubits to a parameterized gate.
61    ///
62    /// Adding control qubits to gates with `Param::Ref` parameters is not supported.
63    #[error(
64        "Control parameterized gate: adding control qubits to parameterized gates is not supported."
65    )]
66    ControlParameterizedGate,
67
68    /// The process has already allocated the maximum number of qubits permitted
69    /// by the QPU configuration and no additional qubits can be created.
70    ///
71    /// Returned by [`crate::process::Process::alloc`] when
72    /// `qubit_counter == qpu_config.num_qubits`.
73    #[error(
74        "Qubit allocation limit reached: the process has already allocated all \
75         qubits allowed by the QPU configuration."
76    )]
77    QubitLimitExceeded,
78
79    /// A gate or measurement referenced a qubit index that exceeds the number
80    /// of qubits allocated so far by the current process.
81    ///
82    /// Returned by [`crate::process::Process::append_block`]
83    /// when the maximum qubit index
84    /// referenced inside the block is ≥ `qubit_counter`. Call
85    /// [`crate::process::Process::alloc`] to allocate the required qubits
86    /// before appending the block.
87    #[error(
88        "Qubit index out of range: the gate block references a qubit that has not \
89         been allocated. Call alloc() for each qubit before using it in a block."
90    )]
91    QubitIndexOutOfRange,
92
93    /// An attempt was made to allocate a qubit on a process that has already
94    /// been executed and is in the `Terminated` state.
95    ///
96    /// Returned by [`crate::process::Process::alloc`]
97    /// when `process.state == ProcessState::Terminated`. Create a new
98    /// [`crate::process::Process`] to run additional circuits.
99    #[error(
100        "Process is terminated: qubit allocation is not allowed after a process has \
101         been executed. Create a new Process to run additional circuits."
102    )]
103    ProcessTerminated,
104
105    /// An attempt was made to append a gate block to a process that is not
106    /// in the `AcceptingGate` state.
107    ///
108    /// Returned by [`crate::process::Process::append_block`]
109    /// when the process state is anything other than
110    /// [`crate::process::ProcessState::AcceptingGate`], for example,
111    /// after `sample` or `exp_value` has already been registered in
112    /// batch mode (which advancesthe state to
113    /// [`crate::process::ProcessState::ReadyToExecute`] or
114    /// [`crate::process::ProcessState::AcceptingHamiltonian`]).
115    #[error(
116        "Cannot append gates: the process is not in the AcceptingGate state. \
117         Gates cannot be added after a sample or exp_value request has been \
118         registered in batch mode."
119    )]
120    GateAppendForbidden,
121
122    /// A single-shot mid-circuit measurement was requested but the process is
123    /// not configured for live execution.
124    ///
125    /// Returned by [`crate::process::Process::measure`]
126    /// when `qpu_config.quantum_execution` is
127    /// `Some(QuantumExecution::Batch { .. })` or `None`. Measurements that
128    /// collapse qubit state and return a result inline are only available in
129    /// [`crate::execution::QuantumExecution::Live`] mode.
130    /// Use `sample` for batch-mode histograms.
131    #[error(
132        "Single-shot measurement is unavailable outside of Live execution mode. \
133         Use 'sample' to collect a measurement histogram."
134    )]
135    MeasurementUnavailableInBatch,
136
137    /// A `sample` request was made but cannot be registered in the current
138    /// process state.
139    ///
140    /// Returned by [`crate::process::Process::sample`] in batch mode when
141    /// the process state is not [`crate::process::ProcessState::AcceptingGate`]
142    /// (i.e., a `sample` or `exp_value` request has already
143    /// been registered), or when no execution backend is configured
144    /// (`qpu_config.quantum_execution == None`). Only one `sample` call is
145    /// permitted per batch execution.
146    #[error(
147        "Sampling unavailable: a sample request can only be registered once per batch \
148         execution, and only while the process is in the AcceptingGate state. \
149         Ensure an execution backend is configured and no conflicting request exists."
150    )]
151    SamplingUnavailable,
152
153    /// An `exp_value` request was made but cannot be registered in the current
154    /// process state.
155    ///
156    /// Returned by [`crate::process::Process::exp_value`] in batch mode when the
157    /// process state is not [`crate::process::ProcessState::AcceptingGate`] or
158    /// [`crate::process::ProcessState::AcceptingHamiltonian`], for example, after the
159    /// process has transitioned to `ReadyToExecute` because a `sample` request
160    /// or a gradient-enabled `exp_value` was already registered, or when no
161    /// execution backend is configured.
162    #[error(
163        "Expectation-value unavailable: exp_value can only be registered while the process \
164         is in the AcceptingGate or AcceptingHamiltonian state. It cannot be combined with \
165         a pending sample request, and requires an execution backend to be configured."
166    )]
167    ExpectationValueUnavailable,
168
169    /// A quantum state dump was requested but the process is not configured
170    /// for live execution.
171    ///
172    /// Returned by [`crate::process::Process::dump`]
173    /// when `qpu_config.quantum_execution` is `Some(QuantumExecution::Batch { .. })`
174    /// or `None`. Full state-vector inspection is only available in
175    /// [`crate::execution::QuantumExecution::Live`] mode.
176    #[error(
177        "State dump is unavailable outside of Live execution mode. \
178         Full state-vector inspection via 'dump' requires Live execution."
179    )]
180    DumpUnavailableInBatch,
181
182    /// `execute` was called on a process that has no pending measurement or
183    /// expectation-value request.
184    ///
185    /// Returned by [`crate::process::Process::execute`] when the process state
186    /// is [`crate::process::ProcessState::AcceptingGate`],
187    /// meaning neither `sample` nor `exp_value` has been called since the
188    /// last execution. Register at least one `sample` or `exp_value` request
189    /// before calling `execute` in batch mode.
190    #[error(
191        "No pending measurement: 'execute' requires at least one 'sample' or 'exp_value' \
192         call before it can be invoked. The process is still in the AcceptingGate state."
193    )]
194    NoPendingMeasurement,
195
196    /// `execute` (or an internal `compile`) was called on a process configured
197    /// for live execution.
198    ///
199    /// Returned by [`crate::process::Process::execute`] when
200    /// `qpu_config.quantum_execution` is `Some(QuantumExecution::Live { .. })`.
201    /// In live mode every gate is dispatched to the backend immediately as it
202    /// is appended; there is no deferred compilation step.
203    #[error(
204        "Explicit execution is not supported in Live mode: gates and measurements are \
205         dispatched to the backend immediately as they are appended. \
206         Use Batch execution to compile and run the circuit via 'execute'."
207    )]
208    ExplicitExecuteInLiveMode,
209
210    /// The quantum execution backend reported an unrecoverable failure.
211    ///
212    /// This is a generic error code that backend implementations
213    /// should return for hardware faults, decoherence events, calibration errors,
214    /// or simulator assertion failures.
215    /// It surfaces in Rust through [`KetError::from_error_code`] after the C
216    /// callback returns a non-zero status. Consult the backend's own error
217    /// channel for a more detailed diagnostic.
218    #[error(
219        "Execution failed: the quantum backend reported an unrecoverable error. \
220         Consult the backend's diagnostic output for details."
221    )]
222    ExecutionFailed,
223
224    /// The requested number of measurement shots is out of range for the
225    /// active backend.
226    ///
227    /// Backend implementations should return this error code
228    /// from their `sample` callback when `shots` is zero, exceeds an
229    /// implementation-defined maximum, or otherwise falls outside the
230    /// backend's accepted range. It reaches Rust via [`KetError::from_error_code`].
231    #[error(
232        "Invalid shot count: the number of measurement shots is out of range for \
233         the active backend. Check the backend's accepted minimum and maximum."
234    )]
235    ShotCountInvalid,
236
237    /// A gate produced by the [`crate::execution::NativeGateSet`] translation
238    /// is not supported by the backend.
239    ///
240    /// Custom [`crate::execution::NativeGateSet`] implementations should
241    /// return this error from `translate`, `cnot`, or `swap` when the
242    /// requested operation cannot be expressed in the hardware's instruction
243    /// set. It is also the recommended return code for batch-execution C
244    /// callbacks that receive an unrecognized gate name.
245    #[error(
246        "Unsupported native gate: the gate produced by the NativeGateSet translation \
247         is not recognized by the backend. Ensure the NativeGateSet is correctly \
248         configured for the target hardware."
249    )]
250    NativeGateUnsupported,
251
252    /// A gate instruction from the Libket IR was received by a backend that
253    /// does not accept IR-level gates.
254    ///
255    /// Returned as the default implementation of [`crate::execution::BatchExecution::sample`]
256    /// and [`crate::execution::BatchExecution::exp_value`] when the backend only
257    /// supports native gates. Implementors that accept IR-level gates should
258    /// override those methods instead of propagating this error.
259    #[error("Unsupported gate: gates from the Libket IR is not supported.")]
260    GateUnsupported,
261
262    /// The backend does not support native expectation-value computation but
263    /// [`crate::execution::ExpValueStrategy::Native`] was selected.
264    ///
265    /// Backend callbacks should return this error code from their
266    /// `exp_value` callback when the hardware or simulator cannot compute
267    /// expectation values directly. Switch to
268    /// [`crate::execution::ExpValueStrategy::QubitWiseCommutation`] or
269    /// [`crate::execution::ExpValueStrategy::ClassicalShadows`] to estimate
270    /// expectation values through repeated sampling instead.
271    #[error(
272        "Native expectation-value computation is not supported by this backend. \
273         Use ExpValueStrategy::QubitWiseCommutation or ExpValueStrategy::ClassicalShadows \
274         to estimate expectation values via sampling."
275    )]
276    ExpValueNotSupported,
277
278    /// JSON serialization or deserialization failed in the C API layer.
279    ///
280    /// Emitted when `serde_json` fails to serialize a Rust value to a
281    /// C string, or when a JSON string received from a C callback cannot be
282    /// parsed into the expected Rust type. Verify that the JSON payload
283    /// conforms to the schema expected by the called function.
284    #[error(
285        "Serialization/deserialization error: a JSON payload could not be encoded \
286         or decoded in the C API layer. Ensure the JSON string is valid and matches \
287         the expected schema."
288    )]
289    SerdeError,
290
291    /// The classical shadows bias tuple `(X, Y, Z)` has all three weights set
292    /// to zero, making it impossible to sample a measurement basis.
293    ///
294    /// Returned by [`crate::process::Process::execute`] (via
295    /// `execute_classical_shadows`) when
296    /// [`crate::execution::ExpValueStrategy::ClassicalShadows`] is configured
297    /// with `bias = (0, 0, 0)`. At least one of the three weights must be
298    /// non-zero.
299    #[error(
300        "Invalid classical shadows bias: all three Pauli basis weights (X, Y, Z) are zero. \
301         At least one weight must be non-zero to sample a measurement basis."
302    )]
303    InvalidBias,
304
305    /// The backend does not support native gradient computation but
306    /// [`crate::execution::GradientStrategy::Native`] was selected.
307    ///
308    /// Returned as the default implementation of
309    /// [`crate::execution::BatchExecution::gradient`]. Switch to
310    /// [`crate::execution::GradientStrategy::ParameterShiftRule`] to estimate
311    /// gradients analytically via repeated circuit evaluations.
312    #[error(
313        "Native Gradient Unsupported: The simulator does not support gradient calculation. \
314         Use ParameterShiftRule."
315    )]
316    NativeGradientUnsuported,
317
318    /// An unrecognized error code was received from a C callback.
319    ///
320    /// Returned by [`KetError::from_error_code`] as a catch-all when the
321    /// integer code does not match any known variant. This typically means a
322    /// C-side backend returned a non-standard status code. Treat it as an
323    /// unrecoverable backend error.
324    #[error(
325        "Unknown error: an unrecognized error code was returned by the backend. \
326         This may indicate a version mismatch or a non-standard status code."
327    )]
328    UnknownError,
329}
330
331impl KetError {
332    /// Returns the integer discriminant of this error variant.
333    ///
334    /// The returned value is stable and suitable for FFI use. `Success` always
335    /// returns `0`; all other variants return a positive non-zero value.
336    #[must_use]
337    pub const fn error_code(&self) -> i32 {
338        match self {
339            Self::Success => 0,
340            Self::DuplicateControlQubit => 1,
341            Self::ControlTargetConflict => 2,
342            Self::QubitLimitExceeded => 3,
343            Self::QubitIndexOutOfRange => 4,
344            Self::ProcessTerminated => 5,
345            Self::GateAppendForbidden => 6,
346            Self::MeasurementUnavailableInBatch => 7,
347            Self::SamplingUnavailable => 8,
348            Self::ExpectationValueUnavailable => 9,
349            Self::DumpUnavailableInBatch => 10,
350            Self::NoPendingMeasurement => 11,
351            Self::ExplicitExecuteInLiveMode => 12,
352            Self::ExecutionFailed => 13,
353            Self::ShotCountInvalid => 14,
354            Self::NativeGateUnsupported => 15,
355            Self::ExpValueNotSupported => 16,
356            Self::SerdeError => 17,
357            Self::InvalidBias => 18,
358            Self::ControlParameterizedGate => 19,
359            Self::GateUnsupported => 20,
360            Self::NativeGradientUnsuported => 21,
361            Self::UnknownError => i32::MAX,
362        }
363    }
364
365    /// Reconstructs a `KetError` from its integer discriminant.
366    ///
367    /// Variants that carry additional context (such as qubit indices) are
368    /// reconstructed with sentinel values (`0` for `usize` fields) because the
369    /// context is not preserved in the raw integer code.
370    ///
371    /// # Safety
372    /// The caller must guarantee that `error_code` was produced by a prior call
373    /// to [`KetError::error_code`]. Passing an arbitrary integer is undefined
374    /// behaviour because this function uses [`std::mem::transmute`] internally
375    /// for the simple variants and pattern-matches for the contextual ones.
376    #[must_use]
377    pub fn from_error_code(error_code: i32) -> Self {
378        match error_code {
379            0 => Self::Success,
380            1 => Self::DuplicateControlQubit,
381            2 => Self::ControlTargetConflict,
382            3 => Self::QubitLimitExceeded,
383            4 => Self::QubitIndexOutOfRange,
384            5 => Self::ProcessTerminated,
385            6 => Self::GateAppendForbidden,
386            7 => Self::MeasurementUnavailableInBatch,
387            8 => Self::SamplingUnavailable,
388            9 => Self::ExpectationValueUnavailable,
389            10 => Self::DumpUnavailableInBatch,
390            11 => Self::NoPendingMeasurement,
391            12 => Self::ExplicitExecuteInLiveMode,
392            13 => Self::ExecutionFailed,
393            14 => Self::ShotCountInvalid,
394            15 => Self::NativeGateUnsupported,
395            16 => Self::ExpValueNotSupported,
396            17 => Self::SerdeError,
397            18 => Self::InvalidBias,
398            19 => Self::ControlParameterizedGate,
399            20 => Self::GateUnsupported,
400            21 => Self::NativeGradientUnsuported,
401            _ => Self::UnknownError,
402        }
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::KetError;
409
410    #[test]
411    fn success_is_zero() {
412        assert_eq!(KetError::Success.error_code(), 0);
413    }
414
415    #[test]
416    fn error_codes_are_distinct() {
417        let variants = vec![
418            KetError::Success,
419            KetError::DuplicateControlQubit,
420            KetError::ControlTargetConflict,
421            KetError::QubitLimitExceeded,
422            KetError::QubitIndexOutOfRange,
423            KetError::ProcessTerminated,
424            KetError::GateAppendForbidden,
425            KetError::MeasurementUnavailableInBatch,
426            KetError::SamplingUnavailable,
427            KetError::ExpectationValueUnavailable,
428            KetError::DumpUnavailableInBatch,
429            KetError::NoPendingMeasurement,
430            KetError::ExplicitExecuteInLiveMode,
431            KetError::ExecutionFailed,
432            KetError::ShotCountInvalid,
433            KetError::NativeGateUnsupported,
434            KetError::ExpValueNotSupported,
435            KetError::SerdeError,
436            KetError::InvalidBias,
437            KetError::ControlParameterizedGate,
438            KetError::GateUnsupported,
439            KetError::NativeGradientUnsuported,
440            KetError::UnknownError,
441        ];
442        let mut codes: Vec<i32> = variants.iter().map(|e| e.error_code()).collect();
443        codes.sort_unstable();
444        codes.dedup();
445        assert_eq!(
446            codes.len(),
447            variants.len(),
448            "Duplicate error codes detected"
449        );
450    }
451
452    #[test]
453    fn roundtrip_simple_variants() {
454        let variants = vec![
455            KetError::Success,
456            KetError::DuplicateControlQubit,
457            KetError::ProcessTerminated,
458            KetError::GateAppendForbidden,
459            KetError::MeasurementUnavailableInBatch,
460            KetError::SamplingUnavailable,
461            KetError::ExpectationValueUnavailable,
462            KetError::DumpUnavailableInBatch,
463            KetError::NoPendingMeasurement,
464            KetError::ExplicitExecuteInLiveMode,
465            KetError::ExpValueNotSupported,
466        ];
467        for v in &variants {
468            let code = v.error_code();
469            let reconstructed = KetError::from_error_code(code);
470            assert_eq!(reconstructed.error_code(), code);
471        }
472    }
473}