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}