Skip to content

Add trait bounds on SimdBase::Element - #302

Merged
Shnatsel merged 8 commits into
linebender:mainfrom
danderson:push-quuvuvtqypuk
Aug 5, 2026
Merged

Add trait bounds on SimdBase::Element#302
Shnatsel merged 8 commits into
linebender:mainfrom
danderson:push-quuvuvtqypuk

Conversation

@danderson

Copy link
Copy Markdown
Contributor

Also add refined bounds on SimdInt and SimdFloat, allowing access to int-only/float-only operations.

The num-traits dependency is optional, and adjusts available float ops based on fearless_simd's std and libm features.

Updates #299


Implementing this is slightly awkward, because we can't add #[cfg] conditionals on supertrait bounds. Given that the SimdElement trait is fairly small, I ended up defining it twice with different bounds.

I could have also defined a helper trait instead, but it ends up being close to the same amount of code, and pollutes the docs with a trait that only exists to work around a missing rustc feature. So, I decided that duplicating the definition was the lesser evil.

Additionally, I defined SimdIntElement and SimdFloatElement traits, and made SimdInt::Element/SimdFloat::Element require those bounds as well. This means generic code can choose to be generic over all SIMD types with restricted operations, or generic over only int/only floats, with the full set of ops available.

One question: num-traits supports the same no_std-friendly features as fearless_simd, and has almost no dependencies. It also should not break existing code, because the SimdElement trait is sealed and this change doesn't alter which types implement it. Given that, should we enable it by default so that T::Element is more useful out of the box?

Below is a big unrolled list of all the core and num-traits traits that the bounds in this patch add, to give an idea of the feature surface available.


SimdElement provides

  • From core:
    • Copy
    • PartialEq
    • Add
    • Sub
    • Mul
    • Div
    • Rem
    • AddAssign
    • SubAssign
    • MulAssign
    • DivAssign
    • RemAssign
  • From num-traits:
    • Zero (additive identity value)
    • One (multiplicative identity value)
    • FromPrimitive (faillible cast from primitive int/float types)
    • ToPrimitive (faillible cast to primitive int/float types)
    • NumCast (::from(impl ToPrimitive))
    • Bounded (min_value() and max_value())
    • FromBytes (from_{be,le,ne}_bytes())
    • ToBytes (to_{be,le,ne}_bytes())

SimdIntElement additionally provides

  • From core:
    • PartialOrd
    • Ord
    • Eq
    • Not
    • BitAnd
    • BitOr
    • BitXor
    • Shl
    • Shr
  • From num-traits:
    • PrimInt (19 methods)
    • CheckedAdd
    • CheckedSub
    • CheckedMul
    • CheckedDiv
    • CheckedNeg
    • CheckedShl
    • CheckedShr
    • CheckedRem
    • WrappingAdd
    • WrappingSub
    • WrappingMul
    • WrappingNeg
    • WrappingShl
    • WrappingShr
    • SaturatingAdd
    • SaturatingSub
    • SaturatingMul
    • MulAdd
    • MulAddAssign

SimdFloatElement additionally provides

  • From core:
    • PartialOrd
    • Neg
  • From num-traits:
    • Float (60 methods with std/libm, 31 without)

@danderson

Copy link
Copy Markdown
Contributor Author

As I try to hack up my bitpacking code with this new support: one annoyance is that I hoped that trait SimdInt<S>: SimdBase<S, Element: SimdIntElement> would make the compiler understand that all the traits implied by SimdIntElement are available on T::Element in generic callers. Alas, this does not appear to be the case, I have to add an explicit where T::Element: SimdIntElement.

I don't know enough deep Rust syntax to know if there's a way to express what I want here, short of defining a separate IntElement associated type with all the bounds stuffed on there (and then probably have a bunch of headaches because rustc won't see that T::Element and T::IntElement are identical, so half the method calls won't work without more bounds and assertions, sigh). Adding the bound isn't the end of the world in my case, it's just slightly annoying because I was hoping that just specifying a bound on SimdInt/SimdFloat would widen the available operations automagically. Advice welcome if there's a way around this.

@danderson

Copy link
Copy Markdown
Contributor Author

For reference, the code sample I gave in #299, updated for this PR and which now compiles:

#[inline(always)]
fn unpack_aligned<S: Simd, T: SimdInt<S>>(
    simd: S,
    mut w: T,
    bits_per_element: usize,
    reference: T::Element,
    out: &mut [T::Element],
) where
    T::Element: SimdIntElement,
{
    let mask = (T::Element::one() << bits_per_element) - T::Element::one();
    let count = T::Element::BITS / bits_per_element;
    for i in 0..count {
        ((w & mask) + reference).store_slice(&mut out[i * T::N..(i + 1) * T::N]);
        w >>= bits_per_element as u32;
    }
}

@Shnatsel

Shnatsel commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

I'll take a closer look later. The where T::Element: SimdIntElement woes sound like the same thing I was battling in #300

A trait bound I think is missing is Default, it can be helpful to initialize an array of these to all zeroes for example. Any blockers to adding it?

@Shnatsel

Shnatsel commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

I dumped all trait impls from u8 and f32 and this is what I got:

impl Add<T> for T
impl Add<&T> for T
impl AddAssign<T> for T
impl AddAssign<&T> for T

impl Sub<T> for T
impl Sub<&T> for T
impl SubAssign<T> for T
impl SubAssign<&T> for T

impl Mul<T> for T
impl Mul<&T> for T
impl MulAssign<T> for T
impl MulAssign<&T> for T

impl Div<T> for T
impl Div<&T> for T
impl DivAssign<T> for T
impl DivAssign<&T> for T

impl Rem<T> for T
impl Rem<&T> for T
impl RemAssign<T> for T
impl RemAssign<&T> for T
Clone
Copy
Debug
Default
Display
From<bool>
From<u8>
FromStr
LowerExp
PartialEq
PartialOrd
Product<T>
Product<&T>
SimdCast
SimdElement
Sum<T>
Sum<&T>
UpperExp
UseCloned

A bunch of these are nightly-only, but things like Default look worth adding to me.

@danderson
danderson force-pushed the push-quuvuvtqypuk branch 3 times, most recently from 52193ee to d5d9f4e Compare August 4, 2026 17:37
@danderson

Copy link
Copy Markdown
Contributor Author

Good call, I was focused on just what num-traits provides but there's more good stuff in core! Technically with num-traits enabled you can get default-equivalent behavior with SimdElement::zero(), but it's much less obvious than Default::default().

I added bounds for stuff that is available in core. SimdElement now also has: Clone, Default, Debug, Display, FromStr, LowerExp, UpperExp, PartialOrd, PartialEq (already did with num-traits, but added to the non-numtraits path too), From<bool>. From<u8> cannot be added because i8 doesn't implement it.

I can probably add the binary ops, Sum<T> and Product<T> bounds as well, and also expand the bounds on SimdIntElement and SimdFloatElement when num-traits is disabled, but I need a bit more time to work out which ones are duplicates in the num-traits path, so I'll add those after work today.

@Shnatsel
Shnatsel self-requested a review August 4, 2026 23:38
@danderson
danderson force-pushed the push-quuvuvtqypuk branch 2 times, most recently from bcf487d to 675d8d1 Compare August 5, 2026 03:03
@danderson

Copy link
Copy Markdown
Contributor Author

Okay, I added a ton more trait bounds. At this point unless I missed one, SimdElement has bounds for everything that i* and f* both implement in rust stable, SimdIntElement adds in the things that only i* has, and same for SimdFloatElement.

Only exception is Shl/Shr on int, where I only pass through Rhs=usize, to match what num-traits does. Passing through more Rhs types runs back into #303.

This does mean that if we ever need to expand the set of supported scalar types, it might be painful because the trait bounds are tighly coupled to exactly what rust core implements on the primitives... But otoh major new scalar types that are also supported by SIMD ISAs feels like a breaking change anyway. The only thing I can think of is maybe f16 for AI people doing half-precision stuff. That would require removing From<f32> from SimdFloatElement, when f16 stabilizes in core.

I was a bit worried that the HRTBs to allow binary operations against &T would force more bounds in calling code, but my example bitpacking code above still builds happily with all these new bounds, so it seems okay.

The expansion also means that the element traits are now pretty usable even without num-traits. All the basic math stuff works fine, the major things num-traits adds is wrapping/saturating arithmetic for ints, and access to a bunch of utility math functions (e.g. log, exp, trig, construct a nan, construct an infinity, ...) for floats.

So... yeah, should be a pretty complete set of functionality now 😂 PTAL.

@danderson
danderson force-pushed the push-quuvuvtqypuk branch 2 times, most recently from cb51504 to 78f335c Compare August 5, 2026 03:23
@danderson

Copy link
Copy Markdown
Contributor Author

Also updated the changelog entries and commit msg to describe the new scope of the change, since num-traits is now only necessary if you need access to the additional stuff not exposed by core traits.

@Shnatsel

Shnatsel commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Okay, so apparently all you need to do to remove the where clause in your snippet is to add use num_traits::One;:

use fearless_simd::{Simd, SimdElement, SimdInt};
use num_traits::One;

#[inline(always)]
fn unpack_aligned<S: Simd, T: SimdInt<S>>(
    simd: S,
    mut w: T,
    bits_per_element: usize,
    reference: T::Element,
    out: &mut [T::Element],
) {
    let mask = (T::Element::one() << bits_per_element) - T::Element::one();
    let count = T::Element::BITS / bits_per_element;
    for i in 0..count {
        ((w & mask) + reference).store_slice(&mut out[i * T::N..(i + 1) * T::N]);
        w >>= bits_per_element as u32;
    }
}

We can probably re-export those from fearless_simd::prelude when the num-traits feature is enabled.

@Shnatsel

Shnatsel commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

We did have requests to support f16 vectors. And f16 in std does not support From<f32>, From<i16>, or From<u16>. We're looking to ship 1.0 soon, so I'd rather not commit to these bounds.

@Shnatsel

Shnatsel commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Considering how far we've managed to get without num_traits, I'm now leaning towards dropping it now and only keeping the core impls. @danderson what do you think?

danderson and others added 7 commits August 5, 2026 08:54
SimdElement is now bound by many Rust core numeric and utility traits,
allowing generic code to work with SimdBase::Element values.

Also introduce SimdIntElement and SimdFloatElement subtraits, which
add even more numeric and utility operations that only work on
ints or floats respectively.

Finally, add an optional dependency on num-traits. When the 'num-traits'
feature is enabled, SimdElement/SimdIntElement/SimdFloatElement get
even more trait bounds for functionality not covered by traits in
Rust core.

Updates linebender#299

Signed-off-by: David Anderson <dave@natulte.net>
@danderson

danderson commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Dropping num-traits would be reasonable, I think. Looking at what we lose, the major things I see are Bounded, FromPrimitive/ToPrimitive, FromBytes/ToBytes. zero() and one() can be obtained with From<bool>, so it's still possible to construct very basic T::Element values generically.

Maybe we should add TryFrom and Into/TryInto bounds, as a substitute for FromPrimitive/ToPrimitive? But just From<bool> works for the specific thing I need.

It's also easy to add more bounds in function definitions for extra requirements. Supporting core traits makes it much nicer out of the box. The num-traits functionality would be nice, but I don't strictly need it myself so it's a tradeoff between more usability and maintenance burden. I'm happy either way.

I rebased the stack to get rid of a merge conflict, and added a patch on top to strip num-traits out again, if you decide you prefer to remove it.

The core traits provide enough operations out of the box, and
generic code that needs more can always add more bounds at the
call site.

Signed-off-by: David Anderson <dave@natulte.net>
@danderson danderson changed the title Add optional num-traits bounds on SimdBase::Element Add trait bounds on SimdBase::Element Aug 5, 2026
@Shnatsel

Shnatsel commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Yeah, I kinda want FromPrimitive/ToPrimitive, but I'm not sure depending on num-traits for this is the best path forward. So I think I'll merge this as-is and we can easily add extra bounds later since the trait is sealed.

Thank you!

@Shnatsel
Shnatsel enabled auto-merge August 5, 2026 16:21
@Shnatsel
Shnatsel added this pull request to the merge queue Aug 5, 2026
Merged via the queue into linebender:main with commit d2b47c0 Aug 5, 2026
22 checks passed
@danderson
danderson deleted the push-quuvuvtqypuk branch August 6, 2026 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants