Skip to main content

infotheory/api/
context.rs

1//! Stateful context and session API surface.
2
3use super::compression::{NcdVariant, try_ncd_bytes_backend};
4use super::generation::{GenerationRng, pick_generated_byte, try_generate_rate_backend_chain};
5use super::metrics::{
6    empirical_entropy_bytes, try_biased_entropy_rate_backend, try_cross_entropy_rate_backend,
7    try_entropy_rate_backend, try_joint_entropy_rate_backend, try_mutual_information_rate_backend,
8    try_ned_rate_backend, try_nte_rate_backend,
9};
10use super::types::{CompressionBackend, GenerationConfig, GenerationUpdateMode, RateBackend};
11use crate::aligned_prefix;
12use crate::error::{InfotheoryError, InfotheoryResult};
13use crate::mixture::{OnlineBytePredictor, RateBackendPredictorCheckpoint};
14use crate::prediction::{
15    BinaryPrediction, BitOrder, BitStreamSemantics, BytePrefixMass,
16    binary_prediction_from_log_probs,
17};
18use crate::spec::{CanonicalBytes, CompiledCompressionBackend, CompiledRateBackend};
19
20/// Returns the current default information theory context for this thread.
21pub fn get_default_ctx() -> InfotheoryResult<InfotheoryCtx> {
22    crate::get_default_ctx()
23}
24
25/// Sets the current default information theory context for this thread.
26pub fn set_default_ctx(ctx: InfotheoryCtx) {
27    crate::set_default_ctx(ctx);
28}
29
30/// Reusable execution context holding default rate and compression backends.
31#[derive(Clone)]
32pub struct InfotheoryCtx {
33    /// Default rate backend for entropy/rate metrics.
34    pub rate_backend: CompiledRateBackend,
35    /// Default compression backend for NCD/compression primitives.
36    pub compression_backend: CompiledCompressionBackend,
37}
38
39/// Stateful rate-backend session for fitting, conditioning, and continuation.
40pub struct RateBackendSession {
41    predictor: crate::mixture::RateBackendPredictor,
42}
43
44/// Stateful bit-level session over a rate backend.
45///
46/// Byte-packed sessions keep the underlying backend byte-native and expose it
47/// through a lazy prefix-mass view. They therefore require whole-byte stream
48/// boundaries: `total_bits`, when provided, must be a multiple of `8`, and
49/// `finish` must not leave a dangling partial byte. Within one buffered byte,
50/// callers must also stay within either adaptive updates or conditioning-only
51/// updates; switching modes mid-byte is rejected because the backend only
52/// commits whole-byte symbols. Binary-token sessions model each bit either
53/// through the backend's native binary-token application or, for byte-native
54/// backends, by adapting the predictor to the literal byte symbols `0` and `1`
55/// and renormalizing those two choices.
56///
57/// The complete checkpoint/restore contract is available in both Rust and
58/// Python (`infotheory_rs.RateBackendBitSession` with
59/// `RateBackendBitSessionCheckpoint`).
60#[derive(Clone)]
61pub struct RateBackendBitSession {
62    backend_code: CanonicalBytes,
63    predictor: crate::mixture::RateBackendPredictor,
64    semantics: BitStreamSemantics,
65    min_prob: f64,
66    prefix: Option<BufferedBytePrefix>,
67    discardable_scopes: usize,
68}
69
70/// Opaque checkpoint for restoring a [`RateBackendBitSession`].
71///
72/// Checkpoints capture the underlying rate predictor plus any in-flight
73/// byte-prefix state, so they are valid even between byte-packed bits before a
74/// full byte has been committed to the backend.
75///
76/// Snapshot-backed predictors restore their predictive state exactly. Compact
77/// journaled predictors may replay reversible markers during restore, so their
78/// floating-point probabilities are restored up to normal round-off while the
79/// discrete model state and stream position are restored to the checkpoint.
80///
81/// (Python exposes this as `infotheory_rs.RateBackendBitSessionCheckpoint`.)
82#[derive(Clone)]
83pub struct RateBackendBitSessionCheckpoint {
84    backend_code: CanonicalBytes,
85    predictor: crate::mixture::RateBackendPredictorCheckpoint,
86    semantics: BitStreamSemantics,
87    prefix: Option<BufferedBytePrefix>,
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
91enum BufferedByteUpdateMode {
92    Adaptive,
93    Frozen,
94}
95
96impl BufferedByteUpdateMode {
97    fn verb(self) -> &'static str {
98        match self {
99            Self::Adaptive => "adaptive",
100            Self::Frozen => "conditioning-only",
101        }
102    }
103}
104
105#[derive(Clone)]
106struct BufferedBytePrefix {
107    kind: BufferedBytePrefixKind,
108    update_mode: Option<BufferedByteUpdateMode>,
109}
110
111#[derive(Clone)]
112// `BytePrefixMass` is the resident prefix tree for the byte-packed path and is
113// intentionally inline. Native-MSB start checkpoints are boxed below so the
114// small native state does not inline a full predictor checkpoint.
115#[allow(clippy::large_enum_variant)]
116enum BufferedBytePrefixKind {
117    Mass(BytePrefixMass),
118    NativeMsb {
119        // Boxed so partial-byte abort/replay stores a checkpoint out-of-line;
120        // the checkpoint enum itself is pointer-sized (Full holds
121        // Box<RateBackendPredictor>). Allocation is paid only when a native-MSB
122        // prefix needs rollback across lifecycle reset or frozen byte replay.
123        start_checkpoint: Option<Box<RateBackendPredictorCheckpoint>>,
124        symbol: u8,
125        bits: usize,
126    },
127}
128
129impl BufferedBytePrefix {
130    fn new_mass(mass: BytePrefixMass) -> Self {
131        Self {
132            kind: BufferedBytePrefixKind::Mass(mass),
133            update_mode: None,
134        }
135    }
136
137    fn new_native_msb(start_checkpoint: Option<RateBackendPredictorCheckpoint>) -> Self {
138        Self {
139            kind: BufferedBytePrefixKind::NativeMsb {
140                start_checkpoint: start_checkpoint.map(Box::new),
141                symbol: 0,
142                bits: 0,
143            },
144            update_mode: None,
145        }
146    }
147
148    fn has_partial_bits(&self) -> bool {
149        match &self.kind {
150            BufferedBytePrefixKind::Mass(mass) => mass.has_partial_bits(),
151            BufferedBytePrefixKind::NativeMsb { bits, .. } => *bits > 0,
152        }
153    }
154
155    fn record_mode(&mut self, requested: BufferedByteUpdateMode) -> InfotheoryResult<()> {
156        if let Some(active) = self.update_mode
157            && self.has_partial_bits()
158            && active != requested
159        {
160            return Err(InfotheoryError::runtime(format!(
161                "byte-packed bit sessions cannot mix {} and {} updates within the same buffered byte; finish the byte with one mode, use `try_observe_bit`/`try_condition_bit` to handle this error explicitly, or switch to BitStreamSemantics::BinaryTokens for mid-byte mode changes",
162                active.verb(),
163                requested.verb(),
164            )));
165        }
166        self.update_mode = Some(requested);
167        Ok(())
168    }
169}
170
171fn byte_packed_total_symbols(total_bits: Option<u64>) -> Result<Option<u64>, String> {
172    let Some(total_bits) = total_bits else {
173        return Ok(None);
174    };
175    if total_bits % 8 != 0 {
176        return Err(format!(
177            "byte-packed bit streams require a whole number of bytes; got {total_bits} bits. Use BitStreamSemantics::BinaryTokens for arbitrary-length bit streams"
178        ));
179    }
180    Ok(Some(total_bits / 8))
181}
182
183fn total_symbols_for_bit_semantics(
184    total_bits: Option<u64>,
185    semantics: BitStreamSemantics,
186) -> Result<Option<u64>, String> {
187    match semantics {
188        BitStreamSemantics::BytePacked { .. } => byte_packed_total_symbols(total_bits),
189        BitStreamSemantics::BinaryTokens => Ok(total_bits),
190    }
191}
192
193impl RateBackendBitSession {
194    fn observed_native_prefix_bit(symbol: u8, bit_idx: usize) -> bool {
195        (symbol & (1u8 << (7 - bit_idx))) != 0
196    }
197
198    /// Restore to `start`, abort empty native MSB prefix, then unconditionally discard `start`.
199    ///
200    /// Discard only adjusts journal depth; predictor state is already at `start` after restore.
201    fn restore_start_checkpoint_abort_and_discard(
202        &mut self,
203        start: RateBackendPredictorCheckpoint,
204    ) -> Result<bool, String> {
205        self.predictor.restore_checkpoint(&start);
206        let abort_res = self.predictor.abort_empty_native_msb_byte_prefix();
207        self.predictor.discard_checkpoint(start);
208        abort_res
209    }
210
211    fn release_inflight_prefix_checkpoint_for_restore(&mut self) {
212        let Some(prefix) = self.prefix.take() else {
213            return;
214        };
215        if let BufferedBytePrefixKind::NativeMsb {
216            start_checkpoint: Some(checkpoint),
217            ..
218        } = prefix.kind
219        {
220            self.predictor.discard_checkpoint(*checkpoint);
221        }
222    }
223
224    fn discard_inflight_prefix_checkpoint(&mut self) {
225        let Some(prefix) = self.prefix.take() else {
226            return;
227        };
228        match prefix.kind {
229            BufferedBytePrefixKind::Mass(_) => {}
230            BufferedBytePrefixKind::NativeMsb {
231                start_checkpoint: Some(checkpoint),
232                ..
233            } => {
234                self.restore_start_checkpoint_abort_and_discard(*checkpoint)
235                    .expect("native MSB prefix start checkpoint must be empty");
236            }
237            BufferedBytePrefixKind::NativeMsb {
238                start_checkpoint: None,
239                bits,
240                ..
241            } => {
242                if bits == 0 {
243                    self.predictor
244                        .abort_empty_native_msb_byte_prefix()
245                        .expect("empty native MSB prefix abort must succeed");
246                } else {
247                    panic!(
248                        "discarding an adaptive native MSB byte-prefix with observed bits; \
249                         missing prefix start checkpoint"
250                    );
251                }
252            }
253        }
254    }
255
256    fn abandon_inflight_prefix_for_lifecycle_reset(&mut self) {
257        let Some(prefix) = self.prefix.take() else {
258            return;
259        };
260        match prefix.kind {
261            BufferedBytePrefixKind::Mass(_) => {}
262            BufferedBytePrefixKind::NativeMsb {
263                start_checkpoint: Some(checkpoint),
264                ..
265            } => {
266                self.restore_start_checkpoint_abort_and_discard(*checkpoint)
267                    .expect("native MSB prefix start checkpoint must be empty");
268            }
269            BufferedBytePrefixKind::NativeMsb {
270                start_checkpoint: None,
271                bits: 0,
272                ..
273            } => {
274                self.predictor
275                    .abort_empty_native_msb_byte_prefix()
276                    .expect("empty native MSB prefix abort must succeed");
277            }
278            BufferedBytePrefixKind::NativeMsb {
279                start_checkpoint: None,
280                bits: _,
281                ..
282            } => {
283                self.predictor
284                    .abandon_incomplete_native_msb_byte_prefix_for_lifecycle()
285                    .expect("native MSB prefix lifecycle abandonment must succeed");
286            }
287        }
288    }
289
290    /// Make clearing checkpoint journals preserve the byte-prefix invariant:
291    /// `NativeMsb` session state exists only while the predictor has an active
292    /// native MSB prefix. Partial native prefixes are downgraded to the generic
293    /// mass prefix before clearing so their rollback checkpoint can be released.
294    fn normalize_prefix_for_checkpoint_clear(&mut self) -> InfotheoryResult<bool> {
295        let Some(prefix) = self.prefix.take() else {
296            return Ok(true);
297        };
298        let update_mode = prefix.update_mode;
299        match prefix.kind {
300            BufferedBytePrefixKind::Mass(mass) => {
301                self.prefix = Some(BufferedBytePrefix {
302                    kind: BufferedBytePrefixKind::Mass(mass),
303                    update_mode,
304                });
305                Ok(true)
306            }
307            BufferedBytePrefixKind::NativeMsb {
308                start_checkpoint: None,
309                bits: 0,
310                ..
311            } => {
312                self.predictor
313                    .abort_empty_native_msb_byte_prefix()
314                    .map_err(InfotheoryError::runtime)?;
315                Ok(true)
316            }
317            BufferedBytePrefixKind::NativeMsb {
318                start_checkpoint: Some(checkpoint),
319                symbol,
320                bits,
321            } => {
322                let checkpoint = *checkpoint;
323                self.restore_start_checkpoint_abort_and_discard(checkpoint)
324                    .map_err(InfotheoryError::runtime)?;
325
326                if bits == 0 {
327                    return Ok(true);
328                }
329
330                let mut logps = [0.0f64; 256];
331                self.predictor.fill_log_probs(&mut logps);
332
333                let mut mass = BytePrefixMass::from_log_probs(&logps, BitOrder::MsbFirst);
334                for bit_idx in 0..bits {
335                    mass.observe(Self::observed_native_prefix_bit(symbol, bit_idx));
336                }
337                self.prefix = Some(BufferedBytePrefix {
338                    kind: BufferedBytePrefixKind::Mass(mass),
339                    update_mode,
340                });
341                Ok(true)
342            }
343            BufferedBytePrefixKind::NativeMsb {
344                start_checkpoint: None,
345                symbol,
346                bits,
347            } => {
348                self.prefix = Some(BufferedBytePrefix {
349                    kind: BufferedBytePrefixKind::NativeMsb {
350                        start_checkpoint: None,
351                        symbol,
352                        bits,
353                    },
354                    update_mode,
355                });
356                Ok(false)
357            }
358        }
359    }
360
361    /// Create a bit session from an explicit compiled backend.
362    ///
363    /// For [`BitStreamSemantics::BytePacked`], `total_bits` must be `None` or a
364    /// multiple of `8`.
365    pub fn from_backend(
366        backend: CompiledRateBackend,
367        total_bits: Option<u64>,
368        semantics: BitStreamSemantics,
369    ) -> InfotheoryResult<Self> {
370        Self::from_backend_with_min_prob(
371            backend,
372            total_bits,
373            semantics,
374            crate::mixture::DEFAULT_MIN_PROB,
375        )
376    }
377
378    pub(crate) fn from_backend_with_min_prob(
379        backend: CompiledRateBackend,
380        total_bits: Option<u64>,
381        semantics: BitStreamSemantics,
382        min_prob: f64,
383    ) -> InfotheoryResult<Self> {
384        let total_symbols = total_symbols_for_bit_semantics(total_bits, semantics)
385            .map_err(InfotheoryError::runtime)?;
386        let mut predictor = match semantics {
387            BitStreamSemantics::BinaryTokens => {
388                crate::runtime::build_rate_backend_binary_token_predictor(&backend, min_prob)
389            }
390            BitStreamSemantics::BytePacked { .. } => {
391                if !backend.supports_byte_prefix_mass() {
392                    return Err(InfotheoryError::invalid_backend_config(format!(
393                        "backend '{}' does not support BitStreamSemantics::BytePacked",
394                        backend.canonical_name()
395                    )));
396                }
397                if !backend.supports_efficient_byte_packed_bit_sessions() {
398                    return Err(InfotheoryError::invalid_backend_config(format!(
399                        "backend '{}' can expose byte probabilities but does not support efficient BitStreamSemantics::BytePacked sessions; use BitStreamSemantics::BinaryTokens or a backend with native or cached byte-prefix support",
400                        backend.canonical_name()
401                    )));
402                }
403                crate::runtime::build_rate_backend_predictor(&backend, min_prob)
404            }
405        }
406        .map_err(InfotheoryError::invalid_backend_config)?;
407        predictor
408            .begin_stream(total_symbols)
409            .map_err(InfotheoryError::runtime)?;
410        let backend_code = backend.canonical_bytes().clone();
411        Ok(Self {
412            backend_code,
413            predictor,
414            semantics,
415            min_prob,
416            prefix: None,
417            discardable_scopes: 0,
418        })
419    }
420
421    /// Create a bit session from a wrapper backend spec.
422    ///
423    /// For [`BitStreamSemantics::BytePacked`], `total_bits` must be `None` or a
424    /// multiple of `8`.
425    pub fn from_spec(
426        backend: RateBackend,
427        total_bits: Option<u64>,
428        semantics: BitStreamSemantics,
429    ) -> InfotheoryResult<Self> {
430        let compiled = backend
431            .compile()
432            .map_err(|err| InfotheoryError::invalid_backend_config(err.to_string()))?;
433        Self::from_backend(compiled, total_bits, semantics)
434    }
435
436    /// Predict the next bit without updating state.
437    pub fn predict_bit(&mut self) -> BinaryPrediction {
438        match self.semantics {
439            BitStreamSemantics::BinaryTokens => binary_prediction_from_log_probs(
440                self.predictor.log_prob(0),
441                self.predictor.log_prob(1),
442                self.min_prob,
443            ),
444            BitStreamSemantics::BytePacked { order } => {
445                if self.prefix.is_none()
446                    && order == BitOrder::MsbFirst
447                    && self.predictor.has_native_msb_byte_prefix()
448                {
449                    self.try_begin_native_msb_prefix(None).unwrap_or_else(|err| {
450                        panic!(
451                            "RateBackendPredictor failed to begin native MSB byte-prefix: {err} \
452                             (contract violation; BytePrefixMass fallback is not safe on Err)"
453                        )
454                    });
455                }
456                self.ensure_mass_prefix(order);
457                let native_bits = match &self.prefix.as_ref().expect("prefix initialized").kind {
458                    BufferedBytePrefixKind::Mass(mass) => return mass.prediction(),
459                    BufferedBytePrefixKind::NativeMsb { bits, .. } => *bits,
460                };
461                let p1 = self
462                    .predictor
463                    .native_msb_prefix_prob_one(native_bits)
464                    .expect(
465                        "native_msb_prefix_prob_one failed or returned error for active NativeMsb \
466                         prefix (RateBackendBitSession invariant; predictors must uphold finite \
467                         contract or report via Result consistently)",
468                    );
469                BinaryPrediction::from_prob_one(p1, self.min_prob)
470            }
471        }
472    }
473
474    /// Convenience prediction for `P(bit = 1)`.
475    pub fn predict_one(&mut self) -> f64 {
476        self.predict_bit().p1
477    }
478
479    /// Capture a reversible checkpoint for later restoration.
480    ///
481    /// Backends that store full snapshots restore bit-identical floating-point
482    /// predictions. Backends that use compact reversible journals may differ by
483    /// a few ULP after restore because floating-point accumulators are replayed
484    /// instead of cloned byte-for-byte.
485    pub fn checkpoint(&mut self) -> RateBackendBitSessionCheckpoint {
486        debug_assert_eq!(
487            self.discardable_scopes, 0,
488            "RateBackendBitSession checkpoints are invalid inside discardable simulation scopes"
489        );
490        RateBackendBitSessionCheckpoint {
491            backend_code: self.backend_code.clone(),
492            predictor: self.predictor.checkpoint(),
493            semantics: self.semantics,
494            prefix: self.prefix.clone(),
495        }
496    }
497
498    #[cfg(any(feature = "aixi", test))]
499    pub(crate) fn begin_discardable_scope(&mut self) {
500        self.discardable_scopes = self.discardable_scopes.saturating_add(1);
501    }
502
503    #[cfg(feature = "aixi")]
504    pub(crate) fn clear_discardable_scopes(&mut self) {
505        self.discardable_scopes = 0;
506    }
507
508    /// Restore the session to a previously captured checkpoint.
509    ///
510    /// Snapshot-backed predictors restore bit-identical predictions. Compact
511    /// journaled predictors restore the same discrete predictor state and stream
512    /// position, with predictions equal up to floating-point round-off.
513    ///
514    /// A checkpoint is tied to the backend and bit-stream semantics it was
515    /// created from. Restoring a checkpoint into a different bit session is a
516    /// programmer error and returns an explicit runtime error.
517    pub fn restore_checkpoint(
518        &mut self,
519        checkpoint: &RateBackendBitSessionCheckpoint,
520    ) -> InfotheoryResult<()> {
521        if self.backend_code != checkpoint.backend_code || self.semantics != checkpoint.semantics {
522            return Err(InfotheoryError::runtime(
523                "RateBackendBitSession checkpoint belongs to a different backend or bit semantics",
524            ));
525        }
526        self.release_inflight_prefix_checkpoint_for_restore();
527        self.predictor.restore_checkpoint(&checkpoint.predictor);
528        let mut restored_prefix = checkpoint.prefix.clone();
529        if let Some(BufferedBytePrefix {
530            kind:
531                BufferedBytePrefixKind::NativeMsb {
532                    start_checkpoint: Some(start_checkpoint),
533                    symbol,
534                    bits,
535                },
536            ..
537        }) = checkpoint.prefix.as_ref()
538        {
539            let restored_state = self.predictor.checkpoint();
540            self.predictor.restore_checkpoint(start_checkpoint.as_ref());
541            let abort_res = self.predictor.abort_empty_native_msb_byte_prefix();
542            if let Err(err) = abort_res {
543                self.predictor.restore_checkpoint(&restored_state);
544                self.predictor.discard_checkpoint(restored_state);
545                return Err(InfotheoryError::runtime(err));
546            }
547            let fresh_start = self.predictor.native_prefix_start_checkpoint();
548            match self.predictor.begin_native_msb_byte_prefix() {
549                Ok(true) => {}
550                Ok(false) => {
551                    self.predictor.discard_checkpoint(fresh_start);
552                    self.predictor.restore_checkpoint(&restored_state);
553                    self.predictor.discard_checkpoint(restored_state);
554                    return Err(InfotheoryError::runtime(
555                        "stored checkpoint requires native MSB-first byte-prefix support during restore",
556                    ));
557                }
558                Err(err) => {
559                    self.predictor.discard_checkpoint(fresh_start);
560                    self.predictor.restore_checkpoint(&restored_state);
561                    self.predictor.discard_checkpoint(restored_state);
562                    return Err(InfotheoryError::runtime(err));
563                }
564            }
565            for bit_idx in 0..*bits {
566                let bit = Self::observed_native_prefix_bit(*symbol, bit_idx);
567                if let Err(err) = self.predictor.observe_native_msb_prefix_bit(bit_idx, bit) {
568                    self.predictor.restore_checkpoint(&restored_state);
569                    self.predictor.discard_checkpoint(fresh_start);
570                    self.predictor.discard_checkpoint(restored_state);
571                    return Err(InfotheoryError::runtime(err));
572                }
573            }
574            self.predictor.discard_checkpoint(restored_state);
575            if let Some(prefix) = restored_prefix.as_mut()
576                && let BufferedBytePrefixKind::NativeMsb {
577                    start_checkpoint, ..
578                } = &mut prefix.kind
579            {
580                *start_checkpoint = Some(Box::new(fresh_start));
581            }
582            self.prefix = restored_prefix;
583            return Ok(());
584        }
585        self.prefix = restored_prefix;
586        Ok(())
587    }
588
589    /// Clear compact checkpoint journals when no stored checkpoints remain.
590    ///
591    /// This is an optimization hint for backends with journaled checkpoints.
592    /// Calling it while a checkpoint may still be restored violates the
593    /// checkpoint contract. Byte-packed sessions preserve the invariant between
594    /// buffered prefix state and predictor-native prefix state before clearing.
595    pub fn clear_checkpoints_if_supported(&mut self) {
596        match self.normalize_prefix_for_checkpoint_clear() {
597            Ok(true) => self.predictor.clear_checkpoints_if_supported(),
598            Ok(false) => {}
599            Err(err) => {
600                panic!("failed to normalize native byte-prefix before checkpoint clear: {err}")
601            }
602        }
603    }
604
605    /// Predict and then observe one adaptive/fitting bit.
606    pub fn try_step_bit(&mut self, bit: bool) -> InfotheoryResult<BinaryPrediction> {
607        let prediction = self.predict_bit();
608        self.try_observe_bit(bit)?;
609        Ok(prediction)
610    }
611
612    /// Predict and then observe one adaptive/fitting bit.
613    pub fn step_bit(&mut self, bit: bool) -> BinaryPrediction {
614        self.try_step_bit(bit)
615            .unwrap_or_else(|err| panic!("step_bit rejected an invalid bit-session update: {err}"))
616    }
617
618    /// Observe one adaptive/fitting bit.
619    pub fn try_observe_bit(&mut self, bit: bool) -> InfotheoryResult<()> {
620        match self.semantics {
621            BitStreamSemantics::BinaryTokens => {
622                self.predictor.update(u8::from(bit));
623                Ok(())
624            }
625            BitStreamSemantics::BytePacked { order } => {
626                self.update_byte_packed_bit(bit, order, BufferedByteUpdateMode::Adaptive)
627            }
628        }
629    }
630
631    /// Observe one adaptive/fitting bit.
632    pub fn observe_bit(&mut self, bit: bool) {
633        self.try_observe_bit(bit).unwrap_or_else(|err| {
634            panic!("observe_bit rejected an invalid bit-session update: {err}")
635        });
636    }
637
638    /// Advance conditioning state with one bit without fitting/adapting.
639    pub fn try_condition_bit(&mut self, bit: bool) -> InfotheoryResult<()> {
640        match self.semantics {
641            BitStreamSemantics::BinaryTokens => {
642                self.predictor.update_frozen(u8::from(bit));
643                Ok(())
644            }
645            BitStreamSemantics::BytePacked { order } => {
646                self.update_byte_packed_bit(bit, order, BufferedByteUpdateMode::Frozen)
647            }
648        }
649    }
650
651    /// Advance conditioning state with one bit without fitting/adapting.
652    pub fn condition_bit(&mut self, bit: bool) {
653        self.try_condition_bit(bit).unwrap_or_else(|err| {
654            panic!("condition_bit rejected an invalid bit-session update: {err}")
655        });
656    }
657
658    /// Reset dynamic conditioning state while preserving fitted parameters/statistics.
659    pub fn reset_frozen(&mut self, total_bits: Option<u64>) -> InfotheoryResult<()> {
660        let total_symbols = total_symbols_for_bit_semantics(total_bits, self.semantics)
661            .map_err(InfotheoryError::runtime)?;
662        self.abandon_inflight_prefix_for_lifecycle_reset();
663        self.predictor
664            .reset_frozen(total_symbols)
665            .map_err(InfotheoryError::runtime)
666    }
667
668    /// Finalize the underlying stream if the backend needs it.
669    pub fn finish(&mut self) -> InfotheoryResult<()> {
670        if matches!(self.semantics, BitStreamSemantics::BytePacked { .. })
671            && self
672                .prefix
673                .as_ref()
674                .is_some_and(BufferedBytePrefix::has_partial_bits)
675        {
676            return Err(InfotheoryError::runtime(
677                "byte-packed bit streams must finish on a whole-byte boundary; use BitStreamSemantics::BinaryTokens for arbitrary-length bit streams",
678            ));
679        }
680        self.discard_inflight_prefix_checkpoint();
681        self.predictor
682            .finish_stream()
683            .map_err(InfotheoryError::runtime)
684    }
685
686    fn ensure_prefix_for_update(
687        &mut self,
688        order: BitOrder,
689        update_mode: BufferedByteUpdateMode,
690    ) -> InfotheoryResult<()> {
691        if self.prefix.is_some() {
692            return Ok(());
693        }
694        if order == BitOrder::MsbFirst && self.predictor.has_native_msb_byte_prefix() {
695            let checkpoint = if update_mode == BufferedByteUpdateMode::Frozen {
696                Some(self.predictor.native_prefix_start_checkpoint())
697            } else {
698                None
699            };
700            match self.try_begin_native_msb_prefix(checkpoint) {
701                Ok(true) => return Ok(()),
702                Ok(false) => {}
703                Err(err) => return Err(InfotheoryError::runtime(err)),
704            }
705        }
706        self.ensure_mass_prefix(order);
707        Ok(())
708    }
709
710    /// Try to enter native MSB byte-prefix mode for byte-packed sessions.
711    ///
712    /// - `Ok(true)`: native prefix active (`self.prefix` set)
713    /// - `Ok(false)`: caller should use [`Self::ensure_mass_prefix`] (predictor unchanged)
714    /// - `Err`: predictor state-machine failure; caller must not silently fall back
715    fn try_begin_native_msb_prefix(
716        &mut self,
717        start_checkpoint: Option<RateBackendPredictorCheckpoint>,
718    ) -> Result<bool, String> {
719        match self.predictor.begin_native_msb_byte_prefix() {
720            Ok(true) => {
721                self.prefix = Some(BufferedBytePrefix::new_native_msb(start_checkpoint));
722                Ok(true)
723            }
724            Ok(false) => {
725                if let Some(checkpoint) = start_checkpoint {
726                    self.predictor.discard_checkpoint(checkpoint);
727                }
728                Ok(false)
729            }
730            Err(err) => {
731                if let Some(checkpoint) = start_checkpoint {
732                    self.predictor.discard_checkpoint(checkpoint);
733                }
734                Err(err)
735            }
736        }
737    }
738
739    fn ensure_mass_prefix(&mut self, order: BitOrder) {
740        if self.prefix.is_some() {
741            return;
742        }
743        let mut logps = [0.0f64; 256];
744        self.predictor.fill_log_probs(&mut logps);
745        self.prefix = Some(BufferedBytePrefix::new_mass(
746            BytePrefixMass::from_log_probs(&logps, order),
747        ));
748    }
749
750    fn update_byte_packed_bit(
751        &mut self,
752        bit: bool,
753        order: BitOrder,
754        update_mode: BufferedByteUpdateMode,
755    ) -> InfotheoryResult<()> {
756        self.ensure_prefix_for_update(order, update_mode)?;
757        let needs_prefix_checkpoint =
758            update_mode != BufferedByteUpdateMode::Adaptive || self.discardable_scopes == 0;
759        let prefix = self.prefix.as_mut().expect("prefix initialized");
760        prefix.record_mode(update_mode)?;
761        match &mut prefix.kind {
762            BufferedBytePrefixKind::Mass(mass) => {
763                mass.observe(bit);
764                if mass.is_complete() {
765                    let symbol = mass.symbol();
766                    match update_mode {
767                        BufferedByteUpdateMode::Adaptive => self.predictor.update(symbol),
768                        BufferedByteUpdateMode::Frozen => self.predictor.update_frozen(symbol),
769                    }
770                    self.prefix = None;
771                }
772            }
773            BufferedBytePrefixKind::NativeMsb {
774                start_checkpoint,
775                symbol,
776                bits,
777            } => {
778                if start_checkpoint.is_none() && *bits == 0 && needs_prefix_checkpoint {
779                    *start_checkpoint =
780                        Some(Box::new(self.predictor.native_prefix_start_checkpoint()));
781                }
782                match update_mode {
783                    BufferedByteUpdateMode::Adaptive => self
784                        .predictor
785                        .observe_native_msb_prefix_bit(*bits, bit)
786                        .map_err(InfotheoryError::runtime)?,
787                    BufferedByteUpdateMode::Frozen => self
788                        .predictor
789                        .condition_native_msb_prefix_bit_for_rollback(*bits, bit)
790                        .map_err(InfotheoryError::runtime)?,
791                }
792                if bit {
793                    *symbol |= 1u8 << (7 - *bits);
794                }
795                *bits += 1;
796                if *bits == 8 {
797                    let completed_symbol = *symbol;
798                    if update_mode == BufferedByteUpdateMode::Frozen {
799                        let checkpoint_box = start_checkpoint.take().ok_or_else(|| {
800                            InfotheoryError::runtime(
801                                "native byte-prefix frozen update is missing its start checkpoint",
802                            )
803                        })?;
804                        let checkpoint = *checkpoint_box;
805                        self.restore_start_checkpoint_abort_and_discard(checkpoint)
806                            .map_err(InfotheoryError::runtime)?;
807                        self.predictor.update_frozen(completed_symbol);
808                    } else {
809                        self.predictor
810                            .finish_native_msb_byte_prefix(completed_symbol)
811                            .map_err(InfotheoryError::runtime)?;
812                        if let Some(checkpoint) = start_checkpoint.take() {
813                            self.predictor.discard_checkpoint(*checkpoint);
814                        }
815                    }
816                    self.prefix = None;
817                }
818            }
819        }
820        Ok(())
821    }
822}
823
824impl crate::prediction::OnlineBitPredictor for RateBackendBitSession {
825    fn begin_bit_stream(
826        &mut self,
827        total_bits: Option<u64>,
828        semantics: BitStreamSemantics,
829    ) -> Result<(), String> {
830        if semantics != self.semantics {
831            return Err(
832                "bit stream semantics are fixed for a RateBackendBitSession; create a new session"
833                    .to_string(),
834            );
835        }
836        let total_symbols = total_symbols_for_bit_semantics(total_bits, self.semantics)?;
837        self.abandon_inflight_prefix_for_lifecycle_reset();
838        self.predictor.begin_fresh_stream(total_symbols)
839    }
840
841    fn finish_bit_stream(&mut self) -> Result<(), String> {
842        self.finish().map_err(|err| err.to_string())
843    }
844
845    fn bit_prediction(&mut self) -> BinaryPrediction {
846        self.predict_bit()
847    }
848
849    fn update_bit(&mut self, bit: bool) {
850        self.observe_bit(bit);
851    }
852
853    fn update_bit_frozen(&mut self, bit: bool) {
854        self.condition_bit(bit);
855    }
856}
857
858impl RateBackendSession {
859    /// Create a session from an explicit backend.
860    ///
861    /// Algorithmic configuration (such as ROSA's `max_order`) lives inside the
862    /// backend's variant; the session does not take it as an argument.
863    pub fn from_backend(
864        backend: CompiledRateBackend,
865        total_symbols: Option<u64>,
866    ) -> InfotheoryResult<Self> {
867        let mut predictor = crate::runtime::build_rate_backend_predictor_default(&backend)
868            .map_err(InfotheoryError::invalid_backend_config)?;
869        predictor
870            .begin_stream(total_symbols)
871            .map_err(InfotheoryError::runtime)?;
872        Ok(Self { predictor })
873    }
874
875    /// Create a session from a wrapper backend spec.
876    pub fn from_spec(backend: RateBackend, total_symbols: Option<u64>) -> InfotheoryResult<Self> {
877        let compiled = backend
878            .compile()
879            .map_err(|err| InfotheoryError::invalid_backend_config(err.to_string()))?;
880        Self::from_backend(compiled, total_symbols)
881    }
882
883    /// Observe bytes while adapting/fitting the model.
884    pub fn observe(&mut self, data: &[u8]) {
885        for &byte in data {
886            self.predictor.update(byte);
887        }
888    }
889
890    /// Advance conditioning state without changing fitted parameters/statistics.
891    pub fn condition(&mut self, data: &[u8]) {
892        for &byte in data {
893            self.predictor.update_frozen(byte);
894        }
895    }
896
897    /// Reset dynamic conditioning state while preserving fitted parameters/statistics.
898    pub fn reset_frozen(&mut self, total_symbols: Option<u64>) -> InfotheoryResult<()> {
899        self.predictor
900            .reset_frozen(total_symbols)
901            .map_err(InfotheoryError::runtime)
902    }
903
904    /// Start a new stream while preserving each backend's semantic contract.
905    ///
906    /// Backends that support frozen-reset semantics will restart via
907    /// `reset_frozen`. Backends that do not (for example ZPAQ) restart through
908    /// ordinary stream lifecycle hooks instead.
909    pub fn begin_stream(&mut self, total_symbols: Option<u64>) -> InfotheoryResult<()> {
910        self.predictor
911            .begin_fresh_stream(total_symbols)
912            .map_err(InfotheoryError::runtime)
913    }
914
915    /// Fill the 256-way next-byte log-probabilities.
916    pub fn fill_log_probs(&mut self, out: &mut [f64; 256]) {
917        self.predictor.fill_log_probs(out);
918    }
919
920    /// Generate continuation bytes from the current state.
921    pub fn generate_bytes(&mut self, bytes: usize, config: GenerationConfig) -> Vec<u8> {
922        if bytes == 0 {
923            return Vec::new();
924        }
925
926        let mut out = Vec::with_capacity(bytes);
927        let mut logps = [0.0f64; 256];
928        let mut rng = GenerationRng::new(config.seed);
929
930        for _ in 0..bytes {
931            match &mut self.predictor {
932                #[cfg(feature = "backend-rosa")]
933                crate::mixture::RateBackendPredictor::Rosa { .. } => {
934                    for (sym, slot) in logps.iter_mut().enumerate() {
935                        *slot = self.predictor.log_prob(sym as u8);
936                    }
937                }
938                _ => self.predictor.fill_log_probs(&mut logps),
939            }
940            let byte = pick_generated_byte(&logps, config, &mut rng);
941            match config.update_mode {
942                GenerationUpdateMode::Adaptive => self.predictor.update(byte),
943                GenerationUpdateMode::Frozen => self.predictor.update_frozen(byte),
944            }
945            out.push(byte);
946        }
947
948        out
949    }
950
951    /// Finalize the underlying stream if the backend needs it.
952    pub fn finish(&mut self) -> InfotheoryResult<()> {
953        self.predictor
954            .finish_stream()
955            .map_err(InfotheoryError::runtime)
956    }
957}
958
959impl InfotheoryCtx {
960    /// Create the current build's implicit default context.
961    pub fn try_default() -> InfotheoryResult<Self> {
962        Self::from_specs(
963            RateBackend::try_default()?,
964            CompressionBackend::try_default()?,
965        )
966    }
967
968    /// Create a context from explicit rate and compression backends.
969    pub fn new(
970        rate_backend: CompiledRateBackend,
971        compression_backend: CompiledCompressionBackend,
972    ) -> Self {
973        Self {
974            rate_backend,
975            compression_backend,
976        }
977    }
978
979    /// Create a context from wrapper backend specs.
980    pub fn from_specs(
981        rate_backend: RateBackend,
982        compression_backend: CompressionBackend,
983    ) -> InfotheoryResult<Self> {
984        Ok(Self {
985            rate_backend: rate_backend
986                .compile()
987                .map_err(|err| InfotheoryError::invalid_backend_config(err.to_string()))?,
988            compression_backend: compression_backend
989                .compile()
990                .map_err(|err| InfotheoryError::invalid_backend_config(err.to_string()))?,
991        })
992    }
993
994    /// Create a context with adaptive ROSA+ rate backend and ZPAQ compression backend.
995    pub fn try_with_zpaq(method: impl Into<crate::api::ZpaqMethodSpec>) -> InfotheoryResult<Self> {
996        Self::from_specs(
997            RateBackend::RosaPlus { max_order: -1 },
998            CompressionBackend::zpaq(method),
999        )
1000    }
1001
1002    /// Compressed length of one byte slice under this context's compressor.
1003    pub fn try_compress_size(&self, data: &[u8]) -> InfotheoryResult<u64> {
1004        crate::api::compression::try_compress_size_backend(data, &self.compression_backend)
1005    }
1006
1007    /// Compressed length of chained slices under one stream.
1008    pub fn try_compress_size_chain(&self, parts: &[&[u8]]) -> InfotheoryResult<u64> {
1009        crate::api::compression::try_compress_size_chain_backend(parts, &self.compression_backend)
1010    }
1011
1012    /// Create a stateful session for the active rate backend.
1013    pub fn rate_backend_session(
1014        &self,
1015        total_symbols: Option<u64>,
1016    ) -> InfotheoryResult<RateBackendSession> {
1017        RateBackendSession::from_backend(self.rate_backend.clone(), total_symbols)
1018    }
1019
1020    /// Create a stateful bit-level session for the active rate backend.
1021    pub fn rate_backend_bit_session(
1022        &self,
1023        total_bits: Option<u64>,
1024        semantics: BitStreamSemantics,
1025    ) -> InfotheoryResult<RateBackendBitSession> {
1026        RateBackendBitSession::from_backend(self.rate_backend.clone(), total_bits, semantics)
1027    }
1028
1029    /// Fallible entropy-rate estimate for `data` under this context's rate backend.
1030    pub fn try_entropy_rate_bytes(&self, data: &[u8]) -> InfotheoryResult<f64> {
1031        try_entropy_rate_backend(data, &self.rate_backend)
1032    }
1033
1034    /// Fallible biased entropy-rate estimate (plugin variant) for `data`.
1035    pub fn try_biased_entropy_rate_bytes(&self, data: &[u8]) -> InfotheoryResult<f64> {
1036        try_biased_entropy_rate_backend(data, &self.rate_backend)
1037    }
1038
1039    /// Fallible cross entropy of `test_data` under model trained on `train_data`.
1040    pub fn try_cross_entropy_rate_bytes(
1041        &self,
1042        test_data: &[u8],
1043        train_data: &[u8],
1044    ) -> InfotheoryResult<f64> {
1045        try_cross_entropy_rate_backend(test_data, train_data, &self.rate_backend)
1046    }
1047
1048    /// Cross entropy under the active rate backend.
1049    pub fn try_cross_entropy_bytes(
1050        &self,
1051        test_data: &[u8],
1052        train_data: &[u8],
1053    ) -> InfotheoryResult<f64> {
1054        self.try_cross_entropy_rate_bytes(test_data, train_data)
1055    }
1056
1057    /// Fallible joint entropy-rate estimate `H(X,Y)` under aligned-prefix semantics.
1058    pub fn try_joint_entropy_rate_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1059        let (x, y) = aligned_prefix(x, y);
1060        if x.is_empty() {
1061            return Ok(0.0);
1062        }
1063        try_joint_entropy_rate_backend(x, y, &self.rate_backend)
1064    }
1065
1066    /// Fallible conditional entropy-rate estimate `H(X|Y)`.
1067    pub fn try_conditional_entropy_rate_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1068        let (x, y) = aligned_prefix(x, y);
1069        if x.is_empty() {
1070            return Ok(0.0);
1071        }
1072        let h_xy = self.try_joint_entropy_rate_bytes(x, y)?;
1073        let h_y = self.try_entropy_rate_bytes(y)?;
1074        Ok((h_xy - h_y).max(0.0))
1075    }
1076
1077    /// Fallible `H(data | prefix_parts)` by conditioning the active rate backend
1078    /// on an explicit prefix chain.
1079    pub fn try_cross_entropy_conditional_chain(
1080        &self,
1081        prefix_parts: &[&[u8]],
1082        data: &[u8],
1083    ) -> InfotheoryResult<f64> {
1084        crate::runtime::try_cross_entropy_conditional_chain_backend(
1085            prefix_parts,
1086            data,
1087            &self.rate_backend,
1088        )
1089    }
1090
1091    /// Generate a continuation from `prompt` with [`GenerationConfig::default()`].
1092    pub fn try_generate_bytes(&self, prompt: &[u8], bytes: usize) -> InfotheoryResult<Vec<u8>> {
1093        self.try_generate_bytes_with_config(prompt, bytes, GenerationConfig::default())
1094    }
1095
1096    /// Fallible continuation generation from `prompt` using an explicit config.
1097    pub fn try_generate_bytes_with_config(
1098        &self,
1099        prompt: &[u8],
1100        bytes: usize,
1101        config: GenerationConfig,
1102    ) -> InfotheoryResult<Vec<u8>> {
1103        try_generate_rate_backend_chain(&[prompt], bytes, &self.rate_backend, config)
1104    }
1105
1106    /// Generate a continuation after conditioning on an explicit chain of prefix parts.
1107    pub fn try_generate_bytes_conditional_chain(
1108        &self,
1109        prefix_parts: &[&[u8]],
1110        bytes: usize,
1111    ) -> InfotheoryResult<Vec<u8>> {
1112        self.try_generate_bytes_conditional_chain_with_config(
1113            prefix_parts,
1114            bytes,
1115            GenerationConfig::default(),
1116        )
1117    }
1118
1119    /// Fallible continuation generation after conditioning on an explicit chain of prefix parts.
1120    pub fn try_generate_bytes_conditional_chain_with_config(
1121        &self,
1122        prefix_parts: &[&[u8]],
1123        bytes: usize,
1124        config: GenerationConfig,
1125    ) -> InfotheoryResult<Vec<u8>> {
1126        try_generate_rate_backend_chain(prefix_parts, bytes, &self.rate_backend, config)
1127    }
1128
1129    /// NCD between byte slices using this context's compression backend.
1130    pub fn try_ncd_bytes(&self, x: &[u8], y: &[u8], variant: NcdVariant) -> InfotheoryResult<f64> {
1131        try_ncd_bytes_backend(x, y, &self.compression_backend, variant)
1132    }
1133
1134    /// Rate-backend mutual information estimate.
1135    pub fn try_mutual_information_rate_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1136        try_mutual_information_rate_backend(x, y, &self.rate_backend)
1137    }
1138
1139    /// Mutual information under the active rate backend.
1140    pub fn try_mutual_information_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1141        self.try_mutual_information_rate_bytes(x, y)
1142    }
1143
1144    /// Conditional entropy `H(X|Y)` under the active rate backend.
1145    pub fn try_conditional_entropy_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1146        let (x, y) = aligned_prefix(x, y);
1147        let h_xy = self.try_joint_entropy_rate_bytes(x, y)?;
1148        let h_y = self.try_entropy_rate_bytes(y)?;
1149        Ok((h_xy - h_y).max(0.0))
1150    }
1151
1152    /// Normalized entropy distance (NED) under this context's rate backend.
1153    pub fn try_ned_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1154        try_ned_rate_backend(x, y, &self.rate_backend)
1155    }
1156
1157    /// Conservative NED normalization variant under this context's rate backend.
1158    pub fn try_ned_cons_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1159        let (x, y) = aligned_prefix(x, y);
1160        let h_x = self.try_entropy_rate_bytes(x)?;
1161        let h_y = self.try_entropy_rate_bytes(y)?;
1162        let h_xy = self.try_joint_entropy_rate_bytes(x, y)?;
1163        let min_h = h_x.min(h_y);
1164        if h_xy == 0.0 {
1165            Ok(0.0)
1166        } else {
1167            Ok(((h_xy - min_h) / h_xy).clamp(0.0, 1.0))
1168        }
1169    }
1170
1171    /// Normalized transform effort (NTE) under this context's rate backend.
1172    pub fn try_nte_bytes(&self, x: &[u8], y: &[u8]) -> InfotheoryResult<f64> {
1173        try_nte_rate_backend(x, y, &self.rate_backend)
1174    }
1175
1176    /// Intrinsic dependence score in `[0,1]` driven by `(H₀(X) - Ĥ(X)) / H₀(X)`,
1177    /// where `H₀` is the order-0 / empirical entropy and `Ĥ` is the entropy rate
1178    /// produced by this context's rate backend.
1179    pub fn try_intrinsic_dependence_bytes(&self, data: &[u8]) -> InfotheoryResult<f64> {
1180        let h_empirical = empirical_entropy_bytes(data);
1181        if h_empirical < 1e-9 {
1182            return Ok(0.0);
1183        }
1184        let h_rate = self.try_entropy_rate_bytes(data)?;
1185        Ok(((h_empirical - h_rate) / h_empirical).clamp(0.0, 1.0))
1186    }
1187
1188    /// Resistance-to-transformation ratio `I(X;T(X))/H(X)` in `[0,1]` under this context's rate backend.
1189    pub fn try_resistance_to_transformation_bytes(
1190        &self,
1191        x: &[u8],
1192        tx: &[u8],
1193    ) -> InfotheoryResult<f64> {
1194        let (x, tx) = aligned_prefix(x, tx);
1195        let h_x = self.try_entropy_rate_bytes(x)?;
1196        if h_x < 1e-9 {
1197            return Ok(0.0);
1198        }
1199        let mi = self.try_mutual_information_bytes(x, tx)?;
1200        Ok((mi / h_x).clamp(0.0, 1.0))
1201    }
1202}
1203
1204#[cfg(test)]
1205mod tests {
1206    use super::*;
1207    // Re-exported for trait methods (begin_bit_stream etc.) exercised in
1208    // feature-specific tests. Some narrow backend slices do not use the trait
1209    // directly, but broader bit-session test slices do.
1210    #[allow(unused_imports)]
1211    use crate::prediction::OnlineBitPredictor;
1212
1213    #[cfg(feature = "backend-ctw")]
1214    fn ctw_checkpoint_depth(session: &RateBackendBitSession) -> usize {
1215        match &session.predictor {
1216            crate::mixture::RateBackendPredictor::Ctw {
1217                checkpoint_depth, ..
1218            } => *checkpoint_depth,
1219            crate::mixture::RateBackendPredictor::FacCtw {
1220                checkpoint_depth, ..
1221            } => *checkpoint_depth,
1222            _ => panic!("expected ctw predictor"),
1223        }
1224    }
1225
1226    #[cfg(feature = "backend-ctw")]
1227    fn ctw_native_prefix_progress(session: &RateBackendBitSession) -> Option<usize> {
1228        match &session.predictor {
1229            crate::mixture::RateBackendPredictor::Ctw {
1230                native_prefix_progress,
1231                ..
1232            }
1233            | crate::mixture::RateBackendPredictor::FacCtw {
1234                native_prefix_progress,
1235                ..
1236            } => *native_prefix_progress,
1237            _ => panic!("expected ctw predictor"),
1238        }
1239    }
1240
1241    #[cfg(feature = "backend-ctw")]
1242    fn buffered_native_prefix_bits(session: &RateBackendBitSession) -> Option<usize> {
1243        match session.prefix.as_ref().map(|prefix| &prefix.kind) {
1244            Some(BufferedBytePrefixKind::NativeMsb { bits, .. }) => Some(*bits),
1245            _ => None,
1246        }
1247    }
1248
1249    #[cfg(feature = "backend-ctw")]
1250    fn buffered_native_prefix_has_start_checkpoint(
1251        session: &RateBackendBitSession,
1252    ) -> Option<bool> {
1253        match session.prefix.as_ref().map(|prefix| &prefix.kind) {
1254            Some(BufferedBytePrefixKind::NativeMsb {
1255                start_checkpoint, ..
1256            }) => Some(start_checkpoint.is_some()),
1257            _ => None,
1258        }
1259    }
1260
1261    #[cfg(all(feature = "backend-calibrated", feature = "backend-ctw"))]
1262    fn buffered_native_prefix_has_calibrated_start_checkpoint(
1263        session: &RateBackendBitSession,
1264    ) -> Option<bool> {
1265        match session.prefix.as_ref().map(|prefix| &prefix.kind) {
1266            Some(BufferedBytePrefixKind::NativeMsb {
1267                start_checkpoint: Some(checkpoint),
1268                ..
1269            }) => Some(matches!(
1270                checkpoint.as_ref(),
1271                crate::mixture::RateBackendPredictorCheckpoint::CalibratedNativePrefixStart { .. }
1272            )),
1273            Some(BufferedBytePrefixKind::NativeMsb {
1274                start_checkpoint: None,
1275                ..
1276            }) => Some(false),
1277            _ => None,
1278        }
1279    }
1280
1281    #[cfg(feature = "backend-ctw")]
1282    fn new_ctw_byte_packed_session() -> RateBackendBitSession {
1283        RateBackendBitSession::from_spec(
1284            RateBackend::Ctw { depth: 4 },
1285            Some(8),
1286            BitStreamSemantics::BytePacked {
1287                order: BitOrder::MsbFirst,
1288            },
1289        )
1290        .expect("ctw byte-packed session")
1291    }
1292
1293    #[cfg(feature = "backend-ctw")]
1294    fn new_fac_ctw_byte_packed_session() -> RateBackendBitSession {
1295        RateBackendBitSession::from_spec(
1296            RateBackend::FacCtw {
1297                base_depth: 4,
1298                num_percept_bits: 8,
1299                encoding_bits: 8,
1300                msb_first: None,
1301            },
1302            Some(8),
1303            BitStreamSemantics::BytePacked {
1304                order: BitOrder::MsbFirst,
1305            },
1306        )
1307        .expect("fac-ctw byte-packed session")
1308    }
1309
1310    #[cfg(all(feature = "backend-calibrated", feature = "backend-ctw"))]
1311    fn new_calibrated_ctw_byte_packed_session() -> RateBackendBitSession {
1312        RateBackendBitSession::from_spec(
1313            RateBackend::Calibrated {
1314                spec: std::sync::Arc::new(crate::api::CalibratedSpec::new(
1315                    RateBackend::Ctw { depth: 4 },
1316                    crate::api::CalibrationContextKind::TextRepeat,
1317                )),
1318            },
1319            Some(8),
1320            BitStreamSemantics::BytePacked {
1321                order: BitOrder::MsbFirst,
1322            },
1323        )
1324        .expect("calibrated ctw byte-packed session")
1325    }
1326
1327    #[cfg(feature = "backend-ctw")]
1328    #[test]
1329    fn frozen_native_byte_completion_releases_start_checkpoint() {
1330        for mut session in [
1331            new_ctw_byte_packed_session(),
1332            new_fac_ctw_byte_packed_session(),
1333        ] {
1334            for bit in [true, false, true, false, false, true, true, false] {
1335                session
1336                    .try_condition_bit(bit)
1337                    .expect("condition full frozen native byte");
1338            }
1339            assert!(session.prefix.is_none());
1340            assert_eq!(ctw_checkpoint_depth(&session), 0);
1341            assert_eq!(ctw_native_prefix_progress(&session), None);
1342
1343            let pred = session.predict_bit();
1344            assert!(pred.p0.is_finite() && pred.p1.is_finite());
1345            assert_eq!(buffered_native_prefix_bits(&session), Some(0));
1346            assert_eq!(ctw_native_prefix_progress(&session), Some(0));
1347        }
1348    }
1349
1350    #[cfg(feature = "backend-ctw")]
1351    #[test]
1352    fn reset_frozen_discards_inflight_native_prefix_checkpoint() {
1353        let mut session = new_ctw_byte_packed_session();
1354        session.try_condition_bit(true).expect("conditioning bit");
1355        assert_eq!(ctw_checkpoint_depth(&session), 1);
1356
1357        session.reset_frozen(Some(8)).expect("reset frozen");
1358        assert_eq!(ctw_checkpoint_depth(&session), 0);
1359    }
1360
1361    #[cfg(feature = "backend-ctw")]
1362    #[test]
1363    fn begin_bit_stream_discards_inflight_native_prefix_checkpoint() {
1364        let mut session = new_ctw_byte_packed_session();
1365        session.try_condition_bit(true).expect("conditioning bit");
1366        assert_eq!(ctw_checkpoint_depth(&session), 1);
1367
1368        session
1369            .begin_bit_stream(
1370                Some(8),
1371                BitStreamSemantics::BytePacked {
1372                    order: BitOrder::MsbFirst,
1373                },
1374            )
1375            .expect("begin bit stream");
1376        assert_eq!(ctw_checkpoint_depth(&session), 0);
1377    }
1378
1379    #[cfg(feature = "backend-ctw")]
1380    #[test]
1381    fn clear_checkpoints_after_empty_native_restore_drops_prefix_cleanly() {
1382        for mut session in [
1383            new_ctw_byte_packed_session(),
1384            new_fac_ctw_byte_packed_session(),
1385        ] {
1386            let _ = session.predict_bit();
1387            let checkpoint = session.checkpoint();
1388            session
1389                .try_condition_bit(true)
1390                .expect("conditioning bit after checkpoint");
1391
1392            session
1393                .restore_checkpoint(&checkpoint)
1394                .expect("restore empty native prefix checkpoint");
1395            assert_eq!(buffered_native_prefix_bits(&session), Some(0));
1396            assert_eq!(ctw_native_prefix_progress(&session), Some(0));
1397
1398            session.clear_checkpoints_if_supported();
1399            assert!(session.prefix.is_none());
1400            assert_eq!(ctw_checkpoint_depth(&session), 0);
1401            assert_eq!(ctw_native_prefix_progress(&session), None);
1402
1403            let pred = session.predict_bit();
1404            assert!(pred.p0.is_finite() && pred.p1.is_finite());
1405            assert_eq!(buffered_native_prefix_bits(&session), Some(0));
1406            assert_eq!(ctw_native_prefix_progress(&session), Some(0));
1407        }
1408    }
1409
1410    #[cfg(feature = "backend-ctw")]
1411    #[test]
1412    fn clear_checkpoints_converts_partial_native_prefix_to_mass() {
1413        for mut session in [
1414            new_ctw_byte_packed_session(),
1415            new_fac_ctw_byte_packed_session(),
1416        ] {
1417            session.try_condition_bit(true).expect("first prefix bit");
1418            session.try_condition_bit(false).expect("second prefix bit");
1419            assert_eq!(buffered_native_prefix_bits(&session), Some(2));
1420            assert_eq!(ctw_native_prefix_progress(&session), Some(2));
1421
1422            let before_clear = session.predict_bit();
1423            session.clear_checkpoints_if_supported();
1424
1425            assert!(matches!(
1426                session.prefix.as_ref().map(|prefix| &prefix.kind),
1427                Some(BufferedBytePrefixKind::Mass(_))
1428            ));
1429            assert_eq!(ctw_checkpoint_depth(&session), 0);
1430            assert_eq!(ctw_native_prefix_progress(&session), None);
1431
1432            let after_clear = session.predict_bit();
1433            assert!(
1434                (before_clear.p1 - after_clear.p1).abs() <= 1e-12,
1435                "native-to-mass conversion changed prefix prediction: before={} after={}",
1436                before_clear.p1,
1437                after_clear.p1
1438            );
1439
1440            for bit in [true, false, true, false, true, false] {
1441                session
1442                    .try_condition_bit(bit)
1443                    .expect("finish converted mass prefix");
1444            }
1445            session
1446                .finish()
1447                .expect("finish whole byte after conversion");
1448        }
1449    }
1450
1451    #[cfg(feature = "backend-ctw")]
1452    #[test]
1453    fn reset_frozen_rolls_back_adaptive_partial_native_prefix() {
1454        for mut session in [
1455            new_ctw_byte_packed_session(),
1456            new_fac_ctw_byte_packed_session(),
1457        ] {
1458            session.try_observe_bit(true).expect("adaptive prefix bit");
1459            assert_eq!(ctw_checkpoint_depth(&session), 1);
1460            assert_eq!(ctw_native_prefix_progress(&session), Some(1));
1461
1462            session.reset_frozen(Some(8)).expect("reset frozen");
1463
1464            assert_eq!(ctw_checkpoint_depth(&session), 0);
1465            assert_eq!(ctw_native_prefix_progress(&session), None);
1466            let mut fresh = match &session.predictor {
1467                crate::mixture::RateBackendPredictor::Ctw { .. } => new_ctw_byte_packed_session(),
1468                crate::mixture::RateBackendPredictor::FacCtw { .. } => {
1469                    new_fac_ctw_byte_packed_session()
1470                }
1471                _ => unreachable!("test only constructs CTW-family sessions"),
1472            };
1473            let reset_prediction = session.predict_bit();
1474            let fresh_prediction = fresh.predict_bit();
1475            assert!(
1476                (reset_prediction.p1 - fresh_prediction.p1).abs() <= 1e-12,
1477                "partial adaptive native prefix leaked into reset state: reset={} fresh={}",
1478                reset_prediction.p1,
1479                fresh_prediction.p1
1480            );
1481        }
1482    }
1483
1484    #[cfg(feature = "backend-ctw")]
1485    #[test]
1486    fn discardable_scope_avoids_adaptive_native_prefix_checkpoint() {
1487        for mut session in [
1488            new_ctw_byte_packed_session(),
1489            new_fac_ctw_byte_packed_session(),
1490        ] {
1491            session.begin_discardable_scope();
1492            let _ = session.predict_bit();
1493            session
1494                .try_observe_bit(true)
1495                .expect("discardable adaptive prefix bit");
1496
1497            assert_eq!(buffered_native_prefix_bits(&session), Some(1));
1498            assert_eq!(
1499                buffered_native_prefix_has_start_checkpoint(&session),
1500                Some(false),
1501                "discardable adaptive prefixes must not retain restore-only checkpoints",
1502            );
1503            assert_eq!(ctw_checkpoint_depth(&session), 0);
1504
1505            for bit in [false, true, false, true, false, true, false] {
1506                session
1507                    .try_observe_bit(bit)
1508                    .expect("finish discardable adaptive byte");
1509            }
1510            assert!(session.prefix.is_none());
1511            assert_eq!(ctw_checkpoint_depth(&session), 0);
1512            assert_eq!(ctw_native_prefix_progress(&session), None);
1513        }
1514    }
1515
1516    #[cfg(feature = "backend-ctw")]
1517    #[test]
1518    fn discardable_scope_keeps_frozen_native_prefix_checkpoint() {
1519        let mut session = new_ctw_byte_packed_session();
1520        session.begin_discardable_scope();
1521        session
1522            .try_condition_bit(true)
1523            .expect("discardable frozen prefix bit");
1524
1525        assert_eq!(buffered_native_prefix_bits(&session), Some(1));
1526        assert_eq!(
1527            buffered_native_prefix_has_start_checkpoint(&session),
1528            Some(true),
1529            "frozen native prefixes still need a start checkpoint to avoid learning action bytes",
1530        );
1531        assert_eq!(ctw_checkpoint_depth(&session), 1);
1532    }
1533
1534    #[cfg(all(feature = "backend-calibrated", feature = "backend-ctw"))]
1535    #[test]
1536    fn calibrated_native_prefix_supports_update_only_bits() {
1537        let bits = [true, false, true, false, false, true, true, false];
1538
1539        let mut adaptive = new_calibrated_ctw_byte_packed_session();
1540        for bit in bits {
1541            adaptive
1542                .try_observe_bit(bit)
1543                .expect("adaptive calibrated update-only bit");
1544        }
1545        assert!(adaptive.prefix.is_none());
1546        adaptive.finish().expect("finish adaptive calibrated byte");
1547
1548        let mut frozen = new_calibrated_ctw_byte_packed_session();
1549        for bit in bits {
1550            frozen
1551                .try_condition_bit(bit)
1552                .expect("frozen calibrated update-only bit");
1553        }
1554        assert!(frozen.prefix.is_none());
1555        frozen.finish().expect("finish frozen calibrated byte");
1556    }
1557
1558    #[cfg(all(feature = "backend-calibrated", feature = "backend-ctw"))]
1559    #[test]
1560    fn calibrated_adaptive_prefix_uses_lightweight_start_checkpoint() {
1561        let mut session = new_calibrated_ctw_byte_packed_session();
1562        session
1563            .try_observe_bit(true)
1564            .expect("adaptive calibrated prefix bit");
1565        assert_eq!(buffered_native_prefix_bits(&session), Some(1));
1566        assert_eq!(
1567            buffered_native_prefix_has_calibrated_start_checkpoint(&session),
1568            Some(true),
1569            "calibrated adaptive prefixes must store only the wrapped predictor checkpoint"
1570        );
1571
1572        session.reset_frozen(Some(8)).expect("reset frozen");
1573        let mut fresh = new_calibrated_ctw_byte_packed_session();
1574        let reset_prediction = session.predict_bit();
1575        let fresh_prediction = fresh.predict_bit();
1576        assert!(
1577            (reset_prediction.p1 - fresh_prediction.p1).abs() <= 1e-12,
1578            "abandoned calibrated prefix leaked into reset state: reset={} fresh={}",
1579            reset_prediction.p1,
1580            fresh_prediction.p1
1581        );
1582    }
1583
1584    #[cfg(feature = "backend-ctw")]
1585    #[test]
1586    fn begin_bit_stream_rolls_back_adaptive_partial_native_prefix() {
1587        let mut session = new_fac_ctw_byte_packed_session();
1588        session.try_observe_bit(true).expect("adaptive prefix bit");
1589        assert_eq!(ctw_checkpoint_depth(&session), 1);
1590        assert_eq!(ctw_native_prefix_progress(&session), Some(1));
1591
1592        session
1593            .begin_bit_stream(
1594                Some(8),
1595                BitStreamSemantics::BytePacked {
1596                    order: BitOrder::MsbFirst,
1597                },
1598            )
1599            .expect("begin bit stream");
1600
1601        assert_eq!(ctw_checkpoint_depth(&session), 0);
1602        assert_eq!(ctw_native_prefix_progress(&session), None);
1603        session
1604            .try_observe_bit(false)
1605            .expect("native prefix re-entry after adaptive abort");
1606    }
1607
1608    #[cfg(feature = "backend-ctw")]
1609    #[test]
1610    fn restore_checkpoint_discards_abandoned_native_prefix_checkpoint() {
1611        let mut session = new_ctw_byte_packed_session();
1612        let checkpoint = session.checkpoint();
1613        assert_eq!(ctw_checkpoint_depth(&session), 1);
1614
1615        session.try_condition_bit(true).expect("conditioning bit");
1616        assert_eq!(ctw_checkpoint_depth(&session), 2);
1617
1618        session
1619            .restore_checkpoint(&checkpoint)
1620            .expect("restore checkpoint");
1621        assert_eq!(ctw_checkpoint_depth(&session), 1);
1622    }
1623
1624    #[cfg(feature = "backend-ctw")]
1625    #[test]
1626    fn restore_mid_prefix_checkpoint_rebuilds_frozen_native_rollback_point() {
1627        let mut session = new_ctw_byte_packed_session();
1628        session.try_condition_bit(true).expect("conditioning bit");
1629        let checkpoint = session.checkpoint();
1630        assert_eq!(ctw_checkpoint_depth(&session), 2);
1631
1632        session
1633            .try_condition_bit(false)
1634            .expect("conditioning second bit");
1635        session
1636            .restore_checkpoint(&checkpoint)
1637            .expect("restore mid-prefix checkpoint");
1638        assert_eq!(ctw_checkpoint_depth(&session), 2);
1639
1640        for bit in [false, true, false, true, false, true, false] {
1641            session
1642                .try_condition_bit(bit)
1643                .expect("finish restored byte");
1644        }
1645        assert_eq!(ctw_checkpoint_depth(&session), 1);
1646    }
1647
1648    /// When static native MSB support is advertised but dynamic begin negotiation
1649    /// returns `Ok(false)` (e.g. mixture expert lacks checkpoint rollback), `predict_bit`
1650    /// must fall back to the BytePrefixMass path without debug-only false positives.
1651    #[cfg(feature = "backend-mixture")]
1652    #[test]
1653    fn predict_bit_mass_fallback_when_native_begin_returns_false() {
1654        use crate::mixture::{
1655            BayesMixture, DEFAULT_MIN_PROB, ExpertConfig, MixtureRuntime, RateBackendPredictor,
1656        };
1657
1658        #[derive(Clone)]
1659        struct MockNativeNoCheckpoint;
1660
1661        impl OnlineBytePredictor for MockNativeNoCheckpoint {
1662            fn log_prob(&mut self, _symbol: u8) -> f64 {
1663                -(256.0f64).ln()
1664            }
1665
1666            fn update(&mut self, _symbol: u8) {}
1667
1668            fn has_native_msb_byte_prefix(&self) -> bool {
1669                true
1670            }
1671        }
1672
1673        let configs = [ExpertConfig::uniform("native", || {
1674            Box::new(MockNativeNoCheckpoint) as Box<dyn OnlineBytePredictor>
1675        })];
1676        let runtime = MixtureRuntime::Bayes(BayesMixture::new(&configs));
1677        let mut predictor = RateBackendPredictor::Mixture { runtime };
1678        predictor.begin_stream(Some(1)).expect("begin_stream");
1679
1680        let mut session = RateBackendBitSession {
1681            backend_code: CanonicalBytes::from(b"test-native-fallback".to_vec()),
1682            predictor,
1683            semantics: BitStreamSemantics::BytePacked {
1684                order: BitOrder::MsbFirst,
1685            },
1686            min_prob: DEFAULT_MIN_PROB,
1687            prefix: None,
1688            discardable_scopes: 0,
1689        };
1690        let pred = session.predict_bit();
1691        assert!(
1692            (pred.p0 + pred.p1 - 1.0).abs() < 1e-9,
1693            "mass fallback prediction must normalize: p0={} p1={}",
1694            pred.p0,
1695            pred.p1
1696        );
1697        assert!(pred.p1.is_finite());
1698        assert!(matches!(
1699            session.prefix.as_ref().map(|p| &p.kind),
1700            Some(BufferedBytePrefixKind::Mass(_))
1701        ));
1702    }
1703
1704    /// Guards the byte-prefix buffering enum against accidentally embedding a
1705    /// full predictor checkpoint inline in the native-MSB variant.
1706    #[test]
1707    fn record_buffered_byte_prefix_kind_and_related_sizes() {
1708        let byte_prefix_mass = std::mem::size_of::<BytePrefixMass>();
1709        let buffered_kind = std::mem::size_of::<BufferedBytePrefixKind>();
1710        let rate_ckpt = std::mem::size_of::<RateBackendPredictorCheckpoint>();
1711        let opt_rate_ckpt = std::mem::size_of::<Option<RateBackendPredictorCheckpoint>>();
1712        eprintln!(
1713            "BufferedBytePrefixKind sizes: \
1714             BufferedBytePrefixKind={}B (Mass~{}B inline; NativeMsb Option<RateCheckpoint>~{}B; \
1715             RateBackendPredictorCheckpoint enum={}B). start_checkpoint is boxed; Full variant \
1716             is Box<RateBackendPredictor> so compact checkpoint variants stay small.",
1717            buffered_kind, byte_prefix_mass, opt_rate_ckpt, rate_ckpt
1718        );
1719        // Native-MSB frozen rewinds need a predictor checkpoint, but that
1720        // checkpoint may contain full model state. Keeping it boxed prevents the
1721        // cheaper byte-packed mass path from inheriting that storage cost.
1722        assert_eq!(
1723            buffered_kind, byte_prefix_mass,
1724            "post-box BufferedBytePrefixKind (NativeMsb) must not exceed Mass variant size ({} vs {}); \
1725             native-MSB rewind checkpoints must stay out-of-line",
1726            buffered_kind, byte_prefix_mass
1727        );
1728    }
1729}