qubit_codec/transcode/decode_action.rs
1// =============================================================================
2// Copyright (c) 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Decode actions returned by buffered decoder policy hooks.
9
10use core::num::NonZeroUsize;
11
12use super::internal::decode_step::DecodeStep;
13
14/// Action selected after a codec decode attempt fails during `transcode`.
15///
16/// # Type Parameters
17///
18/// - `Value`: Decoded output value type.
19#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
20pub enum DecodeAction<Value> {
21 /// More source units are needed before a value can be produced.
22 ///
23 /// When returned by a decode hook, `required_total` must be greater than
24 /// the hook context's available input count. Returning a value that is
25 /// already satisfied is a hook contract violation and panics in the engine.
26 NeedInput {
27 /// Total units required from the current value start.
28 required_total: usize,
29 },
30
31 /// Consume invalid input without producing output.
32 ///
33 /// When returned by a decode hook, `consumed` must not exceed the hook
34 /// context's available input count. Over-consuming is a hook contract
35 /// violation and panics in the engine.
36 Skip {
37 /// Source units to consume.
38 consumed: NonZeroUsize,
39 },
40
41 /// Produce one value and consume source units.
42 ///
43 /// When returned by a decode hook, `consumed` must not exceed the hook
44 /// context's available input count. Over-consuming is a hook contract
45 /// violation and panics in the engine.
46 Emit {
47 /// Value to write to the output buffer.
48 value: Value,
49 /// Source units to consume.
50 consumed: NonZeroUsize,
51 },
52}
53
54impl<Value> DecodeAction<Value> {
55 /// Converts this policy action into the normalized internal decode attempt.
56 ///
57 /// # Parameters
58 ///
59 /// - `input_index`: Absolute source index where the failed decode started.
60 /// - `available`: Source units visible from `input_index`.
61 ///
62 /// # Returns
63 ///
64 /// Returns the internal decode attempt consumed by buffered decode loops.
65 ///
66 /// # Panics
67 ///
68 /// Panics when a hook returns `NeedInput` with `required_total <=
69 /// available` or a consuming action whose consumed count exceeds
70 /// `available`.
71 #[must_use]
72 #[inline]
73 pub(super) fn into_step(
74 self,
75 input_index: usize,
76 available: usize,
77 ) -> DecodeStep<Value> {
78 match self {
79 Self::NeedInput { required_total } => DecodeStep::need_input(
80 Self::missing_input(required_total, available),
81 available,
82 ),
83 Self::Skip { consumed } => {
84 DecodeStep::skipped(Self::bound_consumed(consumed, available))
85 }
86 Self::Emit { value, consumed } => DecodeStep::decoded(
87 value,
88 Self::bound_consumed(consumed, available),
89 input_index,
90 ),
91 }
92 }
93
94 /// Returns the additional source units required by a need-input action.
95 ///
96 /// # Parameters
97 ///
98 /// - `required_total`: Total source units required from the current value
99 /// start.
100 /// - `available`: Source units already visible at the current value start.
101 ///
102 /// # Returns
103 ///
104 /// Returns a non-zero additional source-unit count.
105 ///
106 /// # Panics
107 ///
108 /// Panics when `required_total <= available`.
109 #[must_use]
110 #[inline(always)]
111 fn missing_input(required_total: usize, available: usize) -> NonZeroUsize {
112 assert!(
113 required_total > available,
114 "DecodeAction::NeedInput required_total must exceed available input",
115 );
116 let additional = required_total - available;
117 qubit_io::nz(additional)
118 }
119
120 /// Validates a policy-reported consumed source-unit count against available
121 /// input.
122 ///
123 /// # Parameters
124 ///
125 /// - `consumed`: Source units requested by the concrete policy.
126 /// - `available`: Source units visible from the current decode cursor.
127 ///
128 /// # Returns
129 ///
130 /// Returns the validated non-zero consumed count.
131 ///
132 /// # Panics
133 ///
134 /// Panics when `available == 0` or when `consumed > available`.
135 #[must_use]
136 #[inline(always)]
137 fn bound_consumed(
138 consumed: NonZeroUsize,
139 available: usize,
140 ) -> NonZeroUsize {
141 assert!(available > 0, "DecodeAction cannot consume empty input");
142 assert!(
143 consumed.get() <= available,
144 "DecodeAction consumed units must not exceed available input",
145 );
146 consumed
147 }
148}