On Tue, Oct 14, 2025 at 3:35 PM Alexandre Courbot <[email protected]> wrote:
[...]
> This might not be too difficult to shoehorn as there is an
> `is_in_bounds!` macro (which we can turn into a const method if that's
> more suitable) that your proc macro could leverage, but I can't help but
> thing that maybe there is a better, more general solution than
> special-casing this?
>
> [1]
> https://lore.kernel.org/rust-for-linux/[email protected]/
I've tried your `BitInt` series, and I think I can generalize (at
least the overflow check) even without the help of `fits_within!`.
Since I can retrieve the number of valid bits and the signedness of a
given integer type (including `BitInt`) while parsing the helper
attributes, I can use that information to check whether casting from
a value of type T to type U would overflow. Here's a sketch using
declarative macro syntax to demonstrate the idea:
```
// I'll write a function that returns equivalent `TokenStream`, which
// is suitable for procedural macros.
macro_rules! check_overflow {
($val:expr, $src_ty:ty, $dst_ty:ty, $dst_nbits:expr) => {{
let val: $src_ty = $val;
// For every integer type (including `BitInt`), its minimum
// value always fits in `i128`.
let dst_min =
(<$dst_ty>::MIN >> (<$dst_ty>::BITS - ($dst_nbits))) as i128;
// For every integer type (including `BitInt`), its maximum
// value always fits in `u128`.
let dst_max =
(<$dst_ty>::MAX >> (<$dst_ty>::BITS - ($dst_nbits))) as u128;
#[allow(unused_comparisons)]
let is_src_signed = <$src_ty>::MIN < 0;
#[allow(unused_comparisons)]
let is_dst_signed = dst_min < 0;
let fits = if is_src_signed && is_dst_signed {
// Casting from a signed value to `i128` does not
// overflow since `i128` is the largest signed
// primitive integer type.
(val as i128) >= dst_min && val <= dst_max
} else if !is_src_signed && !is_dst_signed {
// Casting from an unsigned value to `u128` does not
// overflow since `u128` is the largest unsigned
// primitive integer type.
(val as u128) <= dst_max
} else if is_src_signed && !is_dst_signed {
// Casting from a signed value greater than 0 to `u128`
// does not overflow since since `u128::MAX` is
// greater than `i128::MAX`.
val >= 0 && (val as u128) <= dst_max
} else {
// Casting from an unsigned value to `u128` does not
// overflow since `u128` is the largest unsigned
// primitive integer type.
(val as u128) <= dst_max
};
!fits
}};
// Yes, we can also support `bool`!
($val:expr, $src_ty:ty, bool) => {{
let val: $src_ty = $val;
let fits = val == 0 || val == 1;
!fits
}};
}
// For a `#[repr(i32)]` enum with `#[try_from(BitInt<u8, 4>)]` and
// `#[into(BitInt<u8, 4>)]`:
check_overflow!(Enum::A as i32, i32, u8, 4);
// For a `#[repr(i32)]` enum with `#[try_from(u8)]` and `#[into(u8)]`:
check_overflow!(Enum::A as i32, i32, u8, u8::BITS);
// For a `#[repr(i32)]` enum with `#[try_from(bool)]` and
// `#[into(bool)]`:
check_overflow!(Enum::A as i32, i32, bool);
```
It is somewhat similar to `fits_within!`, but it also cares signedness
differences between source and destination types.
It might help if `BitInt` exposed `MIN` and `MAX` associated constants
in terms of its backing type:
```
macro_rules! impl_max_min {
($($type:ty),+) => {
$(
impl<const NUM_BITS: u32> BitInt<$type, NUM_BITS> {
pub const MIN: $type =
<$type>::MIN >> (<$type>::BITS - NUM_BITS);
pub const MAX: $type =
<$type>::MAX >> (<$type>::BITS - NUM_BITS);
}
)+
};
}
impl_max_min!(u8, u16, u32, u64, i8, i16, i32, i64);
```
... but for consistency with existing primitive integer types, these
constants would ideally be of type `BitInt` instead of the backing
type, which unfortunately limits their usefulness in implementing
`check_overflow!`.
As a final side note: as you're already aware, breaking changes to
`BitInt` will also affect the `TryFrom` and `Into` derive macros, since
these macros depend on certain public APIs provided by `BitInt`. The
same applies to any future custom types that require support from these
macros. That said, I'll do my best to minimize the dependency wherever
possible.
Best Regards,
Jesung