Skip to main content

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}