sim_lib_numbers_core/magnitude.rs
1//! Shared magnitude limits for arbitrary-precision number libraries.
2//!
3//! These helpers keep exact-number domains finite by default while letting
4//! callers thread a different limit through their own numeric budget surfaces.
5
6use sim_kernel::{Error, Result};
7
8/// Default maximum bit length accepted for arbitrary-precision literals and
9/// results.
10pub const DEFAULT_MAX_ARBITRARY_MAGNITUDE_BITS: u64 = 262_144;
11
12/// A finite ceiling for arbitrary-precision literal and result magnitude.
13///
14/// The limit is expressed in binary bits so domains with native bit-length
15/// metadata can enforce it directly. Decimal literal parsers can use
16/// [`MagnitudeLimit::check_decimal_digits`] to conservatively estimate the same
17/// budget before allocating big integer storage.
18///
19/// # Examples
20///
21/// ```
22/// use sim_lib_numbers_core::MagnitudeLimit;
23///
24/// let limit = MagnitudeLimit::new(128);
25/// assert!(limit.check_bits("integer result", 127).is_ok());
26/// assert!(limit.check_decimal_digits("decimal literal", 1_000).is_err());
27/// ```
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub struct MagnitudeLimit {
30 max_bits: u64,
31}
32
33impl MagnitudeLimit {
34 /// Creates a finite arbitrary-precision magnitude limit.
35 pub const fn new(max_bits: u64) -> Self {
36 Self { max_bits }
37 }
38
39 /// Returns the default arbitrary-precision magnitude limit.
40 pub const fn default_arbitrary_precision() -> Self {
41 Self::new(DEFAULT_MAX_ARBITRARY_MAGNITUDE_BITS)
42 }
43
44 /// Returns the maximum allowed magnitude in binary bits.
45 pub const fn max_bits(self) -> u64 {
46 self.max_bits
47 }
48
49 /// Checks a known binary bit length against this limit.
50 pub fn check_bits(self, context: &str, estimated_bits: u64) -> Result<()> {
51 if estimated_bits > self.max_bits {
52 return Err(Error::Eval(format!(
53 "{context} magnitude too large: estimated {estimated_bits} bits exceeds limit {}",
54 self.max_bits
55 )));
56 }
57 Ok(())
58 }
59
60 /// Checks a decimal digit count against this limit, returning the estimated
61 /// binary bit length when it fits.
62 pub fn check_decimal_digits(self, context: &str, digits: usize) -> Result<u64> {
63 let estimated_bits = decimal_digits_to_bits_ceil(digits);
64 self.check_bits(context, estimated_bits)?;
65 Ok(estimated_bits)
66 }
67
68 /// Returns the largest decimal digit count conservatively accepted by this
69 /// bit budget.
70 pub fn max_decimal_digits(self) -> usize {
71 self.max_bits
72 .saturating_mul(1000)
73 .checked_div(3322)
74 .unwrap_or(0)
75 .try_into()
76 .unwrap_or(usize::MAX)
77 }
78}
79
80impl Default for MagnitudeLimit {
81 fn default() -> Self {
82 Self::default_arbitrary_precision()
83 }
84}
85
86/// Conservatively estimates the binary bit length needed for a decimal digit
87/// count.
88///
89/// The estimate rounds up `digits * log2(10)` with a fixed rational
90/// approximation. It is intentionally conservative for budget checks that must
91/// happen before parsing the decimal text into a big integer.
92pub fn decimal_digits_to_bits_ceil(digits: usize) -> u64 {
93 let digits = u64::try_from(digits).unwrap_or(u64::MAX);
94 if digits == 0 {
95 return 0;
96 }
97 digits.saturating_mul(3322).saturating_add(999) / 1000
98}