tuwunel_core/utils/math.rs
1//! Arithmetic checking and numeric conversion helpers.
2//!
3//! Exported macros separate recoverable, expected, and prevalidated arithmetic.
4//! Conversion helpers centralize errors, panics, and deliberate truncation.
5
6mod expect_into;
7mod expected;
8mod tried;
9
10/// Transforms arithmetic expressions into checked operations.
11///
12/// Successful evaluation yields [`Some`], while a failed operation yields
13/// [`None`]. The [`crate::checked!`] macro converts that optional result into
14/// crate error handling.
15pub use checked_ops::checked_ops;
16
17/// Converts values with [`TryFrom`] and panics on failure.
18///
19/// The conversion delegates to [`expect_into`] and panics on failure. Its
20/// destination type can be inferred from the call context.
21pub use self::expect_into::ExpectInto;
22/// Adds checked arithmetic methods that panic on failure.
23///
24/// Each operation panics when its underlying checked operation fails. The trait
25/// covers addition, subtraction, multiplication, division, and remainder.
26pub use self::expected::Expected;
27/// Adds checked arithmetic methods that return a [`Result`].
28///
29/// Each operation returns [`Error::Arithmetic`] when its checked operation
30/// fails. The trait covers addition, subtraction, multiplication, division, and
31/// remainder.
32pub use self::tried::Tried;
33use crate::{Err, Error, Result, debug::type_name, err};
34
35#[expect(
36 clippy::lossy_float_literal,
37 reason = "2^64 is exactly representable"
38)]
39const USIZE_MAX_EXCLUSIVE: f64 = match usize::BITS {
40 | 16 => 65_536.0,
41 | 32 => 4_294_967_296.0,
42 | 64 => 18_446_744_073_709_551_616.0,
43 | _ => panic!("unsupported usize width"),
44};
45
46/// Evaluates a checked arithmetic expression as a [`Result`].
47///
48/// A successful expression returns its value. Overflow or another invalid
49/// operation returns [`Error::Arithmetic`] through a cold error path.
50#[macro_export]
51#[collapse_debuginfo(yes)]
52macro_rules! checked {
53 ($($input:tt)+) => {
54 $crate::utils::math::checked_ops!($($input)+)
55 .ok_or_else(
56 // The compiler will now attempt to inline the math predicate
57 // while moving the error handling out to .text.unlikely.
58 #[cold]
59 || $crate::err!(Arithmetic("operation overflowed or result invalid"))
60 )
61 };
62}
63
64/// Evaluates a checked arithmetic expression and panics on failure.
65///
66/// Use this when failure is not realistically expected but the expression does
67/// not meet the safety bar for `validated!`. The first form accepts a custom
68/// panic message; the second uses a default.
69#[macro_export]
70#[collapse_debuginfo(yes)]
71macro_rules! expected {
72 ($msg:literal, $($input:tt)+) => {
73 $crate::checked!($($input)+).expect($msg)
74 };
75
76 ($($input:tt)+) => {
77 $crate::expected!("arithmetic expression expectation failure", $($input)+)
78 };
79}
80
81/// Evaluates arithmetic with checks enabled only in debug builds.
82///
83/// Debug builds use checked operations and panic when the expression overflows
84/// or is otherwise invalid. Release builds evaluate the expression directly,
85/// so callers must ensure every operation is valid.
86#[cfg(not(debug_assertions))]
87#[macro_export]
88#[collapse_debuginfo(yes)]
89macro_rules! validated {
90 ($($input:tt)+) => {
91 {
92 // TODO rewrite when stmt_expr_attributes is stable
93 #[expect(clippy::arithmetic_side_effects)]
94 let __res = ($($input)+);
95 __res
96 }
97 };
98}
99
100/// Evaluates arithmetic with checks enabled only in debug builds.
101///
102/// Debug builds use checked operations and panic when the expression overflows
103/// or is otherwise invalid. Release builds evaluate the expression directly,
104/// so callers must ensure every operation is valid.
105#[cfg(debug_assertions)]
106#[macro_export]
107#[collapse_debuginfo(yes)]
108macro_rules! validated {
109 ($($input:tt)+) => {
110 $crate::expected!("validated arithmetic expression failed", $($input)+)
111 }
112}
113
114/// Converts a representable nonnegative `f64` to `usize` by truncating toward
115/// zero.
116///
117/// Negative, non-finite, and out-of-range values return [`Error::Arithmetic`].
118/// Negative zero is accepted; valid fractional values are truncated toward
119/// zero.
120#[inline]
121pub fn usize_from_f64(val: f64) -> Result<usize, Error> {
122 if !(0.0..USIZE_MAX_EXCLUSIVE).contains(&val) {
123 return Err!(Arithmetic("Float is not representable as usize"));
124 }
125
126 // SAFETY: The range check proves `val` is finite, nonnegative, and
127 // representable after truncation.
128 Ok(unsafe { val.to_int_unchecked::<usize>() })
129}
130
131/// Converts a Matrix unsigned integer to `usize`.
132///
133/// The conversion is exact. It panics if the value exceeds the platform's
134/// `usize` range.
135#[inline]
136#[must_use]
137pub fn usize_from_ruma(val: ruma::UInt) -> usize {
138 usize::try_from(val).expect("failed conversion from ruma::UInt to usize")
139}
140
141/// Converts a `u64` to a Matrix unsigned integer.
142///
143/// The conversion is exact. It panics if the value exceeds the range supported
144/// by [`ruma::UInt`].
145#[inline]
146#[must_use]
147pub fn ruma_from_u64(val: u64) -> ruma::UInt {
148 ruma::UInt::try_from(val).expect("failed conversion from u64 to ruma::UInt")
149}
150
151/// Converts a `usize` to a Matrix unsigned integer.
152///
153/// The conversion is exact. It panics if the value exceeds the range supported
154/// by [`ruma::UInt`].
155#[inline]
156#[must_use]
157pub fn ruma_from_usize(val: usize) -> ruma::UInt {
158 ruma::UInt::try_from(val).expect("failed conversion from usize to ruma::UInt")
159}
160
161/// Converts a `u64` to `usize` with deliberate truncation when necessary.
162///
163/// Targets with a narrower `usize` discard the high bits. The conversion is
164/// exact when `usize` is at least 64 bits wide.
165#[inline]
166#[must_use]
167#[expect(clippy::as_conversions, clippy::cast_possible_truncation)]
168pub fn usize_from_u64_truncated(val: u64) -> usize { val as usize }
169
170/// Converts a value with [`TryFrom`] and panics if conversion fails.
171///
172/// Successful conversions return the destination value. A failed conversion
173/// terminates with a fixed expectation message.
174#[inline]
175pub fn expect_into<Dst: TryFrom<Src>, Src>(src: Src) -> Dst {
176 try_into(src).expect("failed conversion from Src to Dst")
177}
178
179/// Converts a value with [`TryFrom`] and maps failure to an arithmetic error.
180///
181/// Successful conversions return the destination value unchanged. A failure
182/// records the source and destination type names and discards the original
183/// error.
184#[inline]
185pub fn try_into<Dst: TryFrom<Src>, Src>(src: Src) -> Result<Dst> {
186 Dst::try_from(src).map_err(|_| {
187 err!(Arithmetic(
188 "failed to convert from {} to {}",
189 type_name::<Src>(),
190 type_name::<Dst>()
191 ))
192 })
193}