From ff8ee04b99f1974d33ab02077f877f273ea95762 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Mon, 24 Aug 2026 16:08:06 +0100 Subject: [PATCH 01/20] Make vectorize() #[inline(always)] instead of #[inline] so that it injects exactly one function boundary --- fearless_simd/src/generated/avx2.rs | 2 +- fearless_simd/src/generated/avx512.rs | 2 +- fearless_simd/src/generated/fallback.rs | 2 +- fearless_simd/src/generated/neon.rs | 2 +- fearless_simd/src/generated/sse2.rs | 2 +- fearless_simd/src/generated/sse4_2.rs | 2 +- fearless_simd/src/generated/wasm.rs | 2 +- fearless_simd_gen/src/level.rs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/fearless_simd/src/generated/avx2.rs b/fearless_simd/src/generated/avx2.rs index 0499caaf..bad30bd8 100644 --- a/fearless_simd/src/generated/avx2.rs +++ b/fearless_simd/src/generated/avx2.rs @@ -105,7 +105,7 @@ impl Simd for Avx2 { fn level(self) -> Level { Level::Avx2(self) } - #[inline] + #[inline(always)] fn vectorize R, R>(self, f: F) -> R { #[target_feature( enable = "avx2,bmi1,bmi2,cmpxchg16b,f16c,fma,fxsr,lzcnt,movbe,popcnt,xsave" diff --git a/fearless_simd/src/generated/avx512.rs b/fearless_simd/src/generated/avx512.rs index 348c9fb2..437a157e 100644 --- a/fearless_simd/src/generated/avx512.rs +++ b/fearless_simd/src/generated/avx512.rs @@ -379,7 +379,7 @@ impl Simd for Avx512 { fn level(self) -> Level { Level::Avx512(self) } - #[inline] + #[inline(always)] fn vectorize R, R>(self, f: F) -> R { #[target_feature( enable = "adx,aes,avx512bitalg,avx512bw,avx512cd,avx512dq,avx512f,avx512ifma,avx512vbmi,avx512vbmi2,avx512vl,avx512vnni,avx512vpopcntdq,bmi1,bmi2,cmpxchg16b,fma,fxsr,gfni,lzcnt,movbe,pclmulqdq,popcnt,rdrand,rdseed,sha,vaes,vpclmulqdq,xsave,xsavec,xsaveopt,xsaves" diff --git a/fearless_simd/src/generated/fallback.rs b/fearless_simd/src/generated/fallback.rs index 3e676607..fb46ebf4 100644 --- a/fearless_simd/src/generated/fallback.rs +++ b/fearless_simd/src/generated/fallback.rs @@ -190,7 +190,7 @@ impl Simd for Fallback { #[cfg(not(feature = "force_support_fallback"))] Level::baseline() } - #[inline] + #[inline(always)] fn vectorize R, R>(self, f: F) -> R { fn vectorize_inner R, R>(f: F) -> R { f() diff --git a/fearless_simd/src/generated/neon.rs b/fearless_simd/src/generated/neon.rs index 23f56540..e4418186 100644 --- a/fearless_simd/src/generated/neon.rs +++ b/fearless_simd/src/generated/neon.rs @@ -100,7 +100,7 @@ impl Simd for Neon { fn level(self) -> Level { Level::Neon(self) } - #[inline] + #[inline(always)] fn vectorize R, R>(self, f: F) -> R { #[target_feature(enable = "neon")] fn vectorize_neon R, R>(f: F) -> R { diff --git a/fearless_simd/src/generated/sse2.rs b/fearless_simd/src/generated/sse2.rs index ed0c46c2..6ea22fef 100644 --- a/fearless_simd/src/generated/sse2.rs +++ b/fearless_simd/src/generated/sse2.rs @@ -203,7 +203,7 @@ impl Simd for Sse2 { fn level(self) -> Level { Level::Sse2(self) } - #[inline] + #[inline(always)] fn vectorize R, R>(self, f: F) -> R { #[target_feature(enable = "fxsr,sse,sse2")] fn vectorize_sse2 R, R>(f: F) -> R { diff --git a/fearless_simd/src/generated/sse4_2.rs b/fearless_simd/src/generated/sse4_2.rs index 21870c53..87b7e7c1 100644 --- a/fearless_simd/src/generated/sse4_2.rs +++ b/fearless_simd/src/generated/sse4_2.rs @@ -181,7 +181,7 @@ impl Simd for Sse4_2 { fn level(self) -> Level { Level::Sse4_2(self) } - #[inline] + #[inline(always)] fn vectorize R, R>(self, f: F) -> R { #[target_feature(enable = "fxsr,sse4.2,cmpxchg16b,popcnt")] fn vectorize_sse4_2 R, R>(f: F) -> R { diff --git a/fearless_simd/src/generated/wasm.rs b/fearless_simd/src/generated/wasm.rs index ba07c50e..5ce55edd 100644 --- a/fearless_simd/src/generated/wasm.rs +++ b/fearless_simd/src/generated/wasm.rs @@ -192,7 +192,7 @@ impl Simd for WasmSimd128 { fn level(self) -> Level { Level::WasmSimd128(self) } - #[inline] + #[inline(always)] fn vectorize R, R>(self, f: F) -> R { fn vectorize_inner R, R>(f: F) -> R { f() diff --git a/fearless_simd_gen/src/level.rs b/fearless_simd_gen/src/level.rs index 97c2e017..8acc16c9 100644 --- a/fearless_simd_gen/src/level.rs +++ b/fearless_simd_gen/src/level.rs @@ -211,7 +211,7 @@ pub(crate) trait Level { #level_body } - #[inline] + #[inline(always)] fn vectorize R, R>(self, f: F) -> R { #vectorize_body } From 23476ea369f92975d11f1bdf72bc107feb889629 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Mon, 24 Aug 2026 16:12:26 +0100 Subject: [PATCH 02/20] Add vectorize_inline() --- fearless_simd/src/generated/avx2.rs | 11 ++++++ fearless_simd/src/generated/avx512.rs | 11 ++++++ fearless_simd/src/generated/fallback.rs | 8 +++++ fearless_simd/src/generated/neon.rs | 9 +++++ fearless_simd/src/generated/simd_trait.rs | 41 ++++++++++++++++++++++ fearless_simd/src/generated/sse2.rs | 9 +++++ fearless_simd/src/generated/sse4_2.rs | 9 +++++ fearless_simd/src/generated/wasm.rs | 8 +++++ fearless_simd_gen/src/level.rs | 26 ++++++++++++++ fearless_simd_gen/src/mk_simd_trait.rs | 42 +++++++++++++++++++++++ 10 files changed, 174 insertions(+) diff --git a/fearless_simd/src/generated/avx2.rs b/fearless_simd/src/generated/avx2.rs index bad30bd8..cfa9ef83 100644 --- a/fearless_simd/src/generated/avx2.rs +++ b/fearless_simd/src/generated/avx2.rs @@ -116,6 +116,17 @@ impl Simd for Avx2 { unsafe { vectorize_avx2(f) } } #[inline(always)] + fn vectorize_inline R, R>(self, f: F) -> R { + #[inline] + #[target_feature( + enable = "avx2,bmi1,bmi2,cmpxchg16b,f16c,fma,fxsr,lzcnt,movbe,popcnt,xsave" + )] + fn vectorize_inline_avx2 R, R>(f: F) -> R { + f() + } + unsafe { vectorize_inline_avx2(f) } + } + #[inline(always)] fn splat_f32x4(self, val: f32) -> f32x4 { crate::kernel!( #[inline(always)] diff --git a/fearless_simd/src/generated/avx512.rs b/fearless_simd/src/generated/avx512.rs index 437a157e..6f84c93e 100644 --- a/fearless_simd/src/generated/avx512.rs +++ b/fearless_simd/src/generated/avx512.rs @@ -390,6 +390,17 @@ impl Simd for Avx512 { unsafe { vectorize_avx512(f) } } #[inline(always)] + fn vectorize_inline R, R>(self, f: F) -> R { + #[inline] + #[target_feature( + enable = "adx,aes,avx512bitalg,avx512bw,avx512cd,avx512dq,avx512f,avx512ifma,avx512vbmi,avx512vbmi2,avx512vl,avx512vnni,avx512vpopcntdq,bmi1,bmi2,cmpxchg16b,fma,fxsr,gfni,lzcnt,movbe,pclmulqdq,popcnt,rdrand,rdseed,sha,vaes,vpclmulqdq,xsave,xsavec,xsaveopt,xsaves" + )] + fn vectorize_inline_avx512 R, R>(f: F) -> R { + f() + } + unsafe { vectorize_inline_avx512(f) } + } + #[inline(always)] fn splat_f32x4(self, val: f32) -> f32x4 { crate::kernel!( #[inline(always)] diff --git a/fearless_simd/src/generated/fallback.rs b/fearless_simd/src/generated/fallback.rs index fb46ebf4..1e405d4f 100644 --- a/fearless_simd/src/generated/fallback.rs +++ b/fearless_simd/src/generated/fallback.rs @@ -198,6 +198,14 @@ impl Simd for Fallback { vectorize_inner(f) } #[inline(always)] + fn vectorize_inline R, R>(self, f: F) -> R { + #[inline] + fn vectorize_inline_inner R, R>(f: F) -> R { + f() + } + vectorize_inline_inner(f) + } + #[inline(always)] fn splat_f32x4(self, val: f32) -> f32x4 { [val; 4usize].simd_into(self) } diff --git a/fearless_simd/src/generated/neon.rs b/fearless_simd/src/generated/neon.rs index e4418186..6d8cc36f 100644 --- a/fearless_simd/src/generated/neon.rs +++ b/fearless_simd/src/generated/neon.rs @@ -109,6 +109,15 @@ impl Simd for Neon { unsafe { vectorize_neon(f) } } #[inline(always)] + fn vectorize_inline R, R>(self, f: F) -> R { + #[inline] + #[target_feature(enable = "neon")] + fn vectorize_inline_neon R, R>(f: F) -> R { + f() + } + unsafe { vectorize_inline_neon(f) } + } + #[inline(always)] fn splat_f32x4(self, val: f32) -> f32x4 { crate::kernel!( #[inline(always)] diff --git a/fearless_simd/src/generated/simd_trait.rs b/fearless_simd/src/generated/simd_trait.rs index f1ce7214..c7ae0792 100644 --- a/fearless_simd/src/generated/simd_trait.rs +++ b/fearless_simd/src/generated/simd_trait.rs @@ -213,6 +213,47 @@ pub trait Simd: #[doc = r" assert_eq!(values, [2, 4, 6, 8, 10]);"] #[doc = r" ```"] fn vectorize R, R>(self, f: F) -> R; + #[doc = r" Call function with SIMD instructions enabled, without forcing [inlining](https://matklad.github.io/2021/07/09/inline-in-rust.html)."] + #[doc = r""] + #[doc = r" `vectorize()` will set the correct `#[target_feature]` annotations for the SIMD level."] + #[doc = r" The provided function should be `#[inline(always)]`, otherwise it may not"] + #[doc = r" be able to utilize the best SIMD instructions available."] + #[doc = r" `vectorize()` itself acts as the function boundary in machine code."] + #[doc = r""] + #[doc = r" This is useful when the SIMD implementation has already been selected and you want"] + #[doc = r" to keep a SIMD-generic function outlined instead of forcing the entire function to"] + #[doc = r" be inlined into its caller."] + #[doc = r""] + #[doc = r" # Example"] + #[doc = r""] + #[doc = r" `double_u32s` is deliberately not marked `#[inline(always)]`. Instead, only its"] + #[doc = r" closure is inlined into the target-feature-enabled boundary created by `vectorize()`."] + #[doc = r""] + #[doc = r" ```"] + #[doc = r" use fearless_simd::{dispatch, prelude::*, Level};"] + #[doc = r""] + #[doc = r" fn double_u32s(simd: S, values: &mut [u32]) {"] + #[doc = r" simd.vectorize("] + #[doc = r" #[inline(always)]"] + #[doc = r" || {"] + #[doc = r" let mut chunks = values.chunks_exact_mut(S::u32s::N);"] + #[doc = r" for chunk in &mut chunks {"] + #[doc = r" let value = S::u32s::from_slice(simd, chunk);"] + #[doc = r" (value * 2).store_slice(chunk);"] + #[doc = r" }"] + #[doc = r" for value in chunks.into_remainder() {"] + #[doc = r" *value *= 2;"] + #[doc = r" }"] + #[doc = r" },"] + #[doc = r" );"] + #[doc = r" }"] + #[doc = r""] + #[doc = r" let mut values = [1, 2, 3, 4, 5];"] + #[doc = r" let level = Level::new();"] + #[doc = r" dispatch!(level, simd => double_u32s(simd, &mut values));"] + #[doc = r" assert_eq!(values, [2, 4, 6, 8, 10]);"] + #[doc = r" ```"] + fn vectorize_inline R, R>(self, f: F) -> R; #[doc = "Create a SIMD vector with all elements set to the given value."] fn splat_f32x4(self, val: f32) -> f32x4; #[doc = "Reverse the order of the vector's elements."] diff --git a/fearless_simd/src/generated/sse2.rs b/fearless_simd/src/generated/sse2.rs index 6ea22fef..f7349f93 100644 --- a/fearless_simd/src/generated/sse2.rs +++ b/fearless_simd/src/generated/sse2.rs @@ -212,6 +212,15 @@ impl Simd for Sse2 { unsafe { vectorize_sse2(f) } } #[inline(always)] + fn vectorize_inline R, R>(self, f: F) -> R { + #[inline] + #[target_feature(enable = "fxsr,sse,sse2")] + fn vectorize_inline_sse2 R, R>(f: F) -> R { + f() + } + unsafe { vectorize_inline_sse2(f) } + } + #[inline(always)] fn splat_f32x4(self, val: f32) -> f32x4 { crate::kernel!( #[inline(always)] diff --git a/fearless_simd/src/generated/sse4_2.rs b/fearless_simd/src/generated/sse4_2.rs index 87b7e7c1..f2055919 100644 --- a/fearless_simd/src/generated/sse4_2.rs +++ b/fearless_simd/src/generated/sse4_2.rs @@ -190,6 +190,15 @@ impl Simd for Sse4_2 { unsafe { vectorize_sse4_2(f) } } #[inline(always)] + fn vectorize_inline R, R>(self, f: F) -> R { + #[inline] + #[target_feature(enable = "fxsr,sse4.2,cmpxchg16b,popcnt")] + fn vectorize_inline_sse4_2 R, R>(f: F) -> R { + f() + } + unsafe { vectorize_inline_sse4_2(f) } + } + #[inline(always)] fn splat_f32x4(self, val: f32) -> f32x4 { crate::kernel!( #[inline(always)] diff --git a/fearless_simd/src/generated/wasm.rs b/fearless_simd/src/generated/wasm.rs index 5ce55edd..03a64bb5 100644 --- a/fearless_simd/src/generated/wasm.rs +++ b/fearless_simd/src/generated/wasm.rs @@ -200,6 +200,14 @@ impl Simd for WasmSimd128 { vectorize_inner(f) } #[inline(always)] + fn vectorize_inline R, R>(self, f: F) -> R { + #[inline] + fn vectorize_inline_inner R, R>(f: F) -> R { + f() + } + vectorize_inline_inner(f) + } + #[inline(always)] fn splat_f32x4(self, val: f32) -> f32x4 { f32x4_splat(val).simd_into(self) } diff --git a/fearless_simd_gen/src/level.rs b/fearless_simd_gen/src/level.rs index 8acc16c9..3a2b3baf 100644 --- a/fearless_simd_gen/src/level.rs +++ b/fearless_simd_gen/src/level.rs @@ -175,6 +175,27 @@ pub(crate) trait Level { } }; + let vectorize_inline_body = if let Some(target_features) = self.enabled_target_features() { + let vectorize_inline = + format_ident!("vectorize_inline_{}", self.name().to_ascii_lowercase()); + quote! { + #[inline] + #[target_feature(enable = #target_features)] + fn #vectorize_inline R, R>(f: F) -> R { + f() + } + unsafe { #vectorize_inline(f) } + } + } else { + quote! { + #[inline] + fn vectorize_inline_inner R, R>(f: F) -> R { + f() + } + vectorize_inline_inner(f) + } + }; + let level_body = self.make_level_body(); let mut assoc_types = vec![]; @@ -216,6 +237,11 @@ pub(crate) trait Level { #vectorize_body } + #[inline(always)] + fn vectorize_inline R, R>(self, f: F) -> R { + #vectorize_inline_body + } + #( #[inline(always)] #methods diff --git a/fearless_simd_gen/src/mk_simd_trait.rs b/fearless_simd_gen/src/mk_simd_trait.rs index 5cd38705..44229077 100644 --- a/fearless_simd_gen/src/mk_simd_trait.rs +++ b/fearless_simd_gen/src/mk_simd_trait.rs @@ -186,6 +186,48 @@ pub(crate) fn mk_simd_trait() -> TokenStream { /// assert_eq!(values, [2, 4, 6, 8, 10]); /// ``` fn vectorize R, R>(self, f: F) -> R; + + /// Call function with SIMD instructions enabled, without forcing [inlining](https://matklad.github.io/2021/07/09/inline-in-rust.html). + /// + /// `vectorize()` will set the correct `#[target_feature]` annotations for the SIMD level. + /// The provided function should be `#[inline(always)]`, otherwise it may not + /// be able to utilize the best SIMD instructions available. + /// `vectorize()` itself acts as the function boundary in machine code. + /// + /// This is useful when the SIMD implementation has already been selected and you want + /// to keep a SIMD-generic function outlined instead of forcing the entire function to + /// be inlined into its caller. + /// + /// # Example + /// + /// `double_u32s` is deliberately not marked `#[inline(always)]`. Instead, only its + /// closure is inlined into the target-feature-enabled boundary created by `vectorize()`. + /// + /// ``` + /// use fearless_simd::{dispatch, prelude::*, Level}; + /// + /// fn double_u32s(simd: S, values: &mut [u32]) { + /// simd.vectorize( + /// #[inline(always)] + /// || { + /// let mut chunks = values.chunks_exact_mut(S::u32s::N); + /// for chunk in &mut chunks { + /// let value = S::u32s::from_slice(simd, chunk); + /// (value * 2).store_slice(chunk); + /// } + /// for value in chunks.into_remainder() { + /// *value *= 2; + /// } + /// }, + /// ); + /// } + /// + /// let mut values = [1, 2, 3, 4, 5]; + /// let level = Level::new(); + /// dispatch!(level, simd => double_u32s(simd, &mut values)); + /// assert_eq!(values, [2, 4, 6, 8, 10]); + /// ``` + fn vectorize_inline R, R>(self, f: F) -> R; #( #methods )* } }; From d17e3ebb26cad07f8106cd2834233a18052676b1 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Mon, 24 Aug 2026 16:22:11 +0100 Subject: [PATCH 03/20] Document vectorize() and vectorize_inline() properly --- fearless_simd/src/generated/simd_trait.rs | 52 +++++++++++------------ fearless_simd_gen/src/mk_simd_trait.rs | 52 +++++++++++------------ 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/fearless_simd/src/generated/simd_trait.rs b/fearless_simd/src/generated/simd_trait.rs index c7ae0792..89c97d88 100644 --- a/fearless_simd/src/generated/simd_trait.rs +++ b/fearless_simd/src/generated/simd_trait.rs @@ -174,36 +174,39 @@ pub trait Simd: fn level(self) -> Level; #[doc = r" Call function with SIMD instructions enabled, without forcing [inlining](https://matklad.github.io/2021/07/09/inline-in-rust.html)."] #[doc = r""] + #[doc = r" This is useful for calling SIMD functions annotated `#[inline(always)]`"] + #[doc = r" when you don't want to actually force inlining all the way up to a `dispatch!` call."] + #[doc = r" `vectorize()` acts as the function boundary in machine code."] + #[doc = r""] #[doc = r" `vectorize()` will set the correct `#[target_feature]` annotations for the SIMD level."] #[doc = r" The provided function should be `#[inline(always)]`, otherwise it may not"] #[doc = r" be able to utilize the best SIMD instructions available."] - #[doc = r" `vectorize()` itself acts as the function boundary in machine code."] - #[doc = r""] - #[doc = r" This is useful when the SIMD implementation has already been selected and you want"] - #[doc = r" to keep a SIMD-generic function outlined instead of forcing the entire function to"] - #[doc = r" be inlined into its caller."] #[doc = r""] #[doc = r" # Example"] #[doc = r""] - #[doc = r" `double_u32s` is deliberately not marked `#[inline(always)]`. Instead, only its"] - #[doc = r" closure is inlined into the target-feature-enabled boundary created by `vectorize()`."] + #[doc = r" `double_u32s_inner` is marked `#[inline(always)]`, but `double_u32s` is not."] + #[doc = r" `vectorize()` creates a function boundary so the implementation does not have to be"] + #[doc = r" inlined all the way into the `dispatch!` call."] #[doc = r""] #[doc = r" ```"] #[doc = r" use fearless_simd::{dispatch, prelude::*, Level};"] #[doc = r""] + #[doc = r" #[inline(always)]"] + #[doc = r" fn double_u32s_inner(simd: S, values: &mut [u32]) {"] + #[doc = r" let mut chunks = values.chunks_exact_mut(S::u32s::N);"] + #[doc = r" for chunk in &mut chunks {"] + #[doc = r" let value = S::u32s::from_slice(simd, chunk);"] + #[doc = r" (value * 2).store_slice(chunk);"] + #[doc = r" }"] + #[doc = r" for value in chunks.into_remainder() {"] + #[doc = r" *value *= 2;"] + #[doc = r" }"] + #[doc = r" }"] + #[doc = r""] #[doc = r" fn double_u32s(simd: S, values: &mut [u32]) {"] #[doc = r" simd.vectorize("] #[doc = r" #[inline(always)]"] - #[doc = r" || {"] - #[doc = r" let mut chunks = values.chunks_exact_mut(S::u32s::N);"] - #[doc = r" for chunk in &mut chunks {"] - #[doc = r" let value = S::u32s::from_slice(simd, chunk);"] - #[doc = r" (value * 2).store_slice(chunk);"] - #[doc = r" }"] - #[doc = r" for value in chunks.into_remainder() {"] - #[doc = r" *value *= 2;"] - #[doc = r" }"] - #[doc = r" },"] + #[doc = r" || double_u32s_inner(simd, values),"] #[doc = r" );"] #[doc = r" }"] #[doc = r""] @@ -213,27 +216,24 @@ pub trait Simd: #[doc = r" assert_eq!(values, [2, 4, 6, 8, 10]);"] #[doc = r" ```"] fn vectorize R, R>(self, f: F) -> R; - #[doc = r" Call function with SIMD instructions enabled, without forcing [inlining](https://matklad.github.io/2021/07/09/inline-in-rust.html)."] + #[doc = r" Call function with SIMD instructions enabled."] #[doc = r""] - #[doc = r" `vectorize()` will set the correct `#[target_feature]` annotations for the SIMD level."] + #[doc = r" This is useful for creating SIMD functions that do not have to be annotated `#[inline(always)]`."] + #[doc = r""] + #[doc = r" `vectorize_inline()` will set the correct `#[target_feature]` annotations for the SIMD level."] #[doc = r" The provided function should be `#[inline(always)]`, otherwise it may not"] #[doc = r" be able to utilize the best SIMD instructions available."] - #[doc = r" `vectorize()` itself acts as the function boundary in machine code."] - #[doc = r""] - #[doc = r" This is useful when the SIMD implementation has already been selected and you want"] - #[doc = r" to keep a SIMD-generic function outlined instead of forcing the entire function to"] - #[doc = r" be inlined into its caller."] #[doc = r""] #[doc = r" # Example"] #[doc = r""] #[doc = r" `double_u32s` is deliberately not marked `#[inline(always)]`. Instead, only its"] - #[doc = r" closure is inlined into the target-feature-enabled boundary created by `vectorize()`."] + #[doc = r" closure is inlined into the target-feature-enabled context created by `vectorize_inline()`."] #[doc = r""] #[doc = r" ```"] #[doc = r" use fearless_simd::{dispatch, prelude::*, Level};"] #[doc = r""] #[doc = r" fn double_u32s(simd: S, values: &mut [u32]) {"] - #[doc = r" simd.vectorize("] + #[doc = r" simd.vectorize_inline("] #[doc = r" #[inline(always)]"] #[doc = r" || {"] #[doc = r" let mut chunks = values.chunks_exact_mut(S::u32s::N);"] diff --git a/fearless_simd_gen/src/mk_simd_trait.rs b/fearless_simd_gen/src/mk_simd_trait.rs index 44229077..e02cbb49 100644 --- a/fearless_simd_gen/src/mk_simd_trait.rs +++ b/fearless_simd_gen/src/mk_simd_trait.rs @@ -147,36 +147,39 @@ pub(crate) fn mk_simd_trait() -> TokenStream { /// Call function with SIMD instructions enabled, without forcing [inlining](https://matklad.github.io/2021/07/09/inline-in-rust.html). /// + /// This is useful for calling SIMD functions annotated `#[inline(always)]` + /// when you don't want to actually force inlining all the way up to a `dispatch!` call. + /// `vectorize()` acts as the function boundary in machine code. + /// /// `vectorize()` will set the correct `#[target_feature]` annotations for the SIMD level. /// The provided function should be `#[inline(always)]`, otherwise it may not /// be able to utilize the best SIMD instructions available. - /// `vectorize()` itself acts as the function boundary in machine code. - /// - /// This is useful when the SIMD implementation has already been selected and you want - /// to keep a SIMD-generic function outlined instead of forcing the entire function to - /// be inlined into its caller. /// /// # Example /// - /// `double_u32s` is deliberately not marked `#[inline(always)]`. Instead, only its - /// closure is inlined into the target-feature-enabled boundary created by `vectorize()`. + /// `double_u32s_inner` is marked `#[inline(always)]`, but `double_u32s` is not. + /// `vectorize()` creates a function boundary so the implementation does not have to be + /// inlined all the way into the `dispatch!` call. /// /// ``` /// use fearless_simd::{dispatch, prelude::*, Level}; /// + /// #[inline(always)] + /// fn double_u32s_inner(simd: S, values: &mut [u32]) { + /// let mut chunks = values.chunks_exact_mut(S::u32s::N); + /// for chunk in &mut chunks { + /// let value = S::u32s::from_slice(simd, chunk); + /// (value * 2).store_slice(chunk); + /// } + /// for value in chunks.into_remainder() { + /// *value *= 2; + /// } + /// } + /// /// fn double_u32s(simd: S, values: &mut [u32]) { /// simd.vectorize( /// #[inline(always)] - /// || { - /// let mut chunks = values.chunks_exact_mut(S::u32s::N); - /// for chunk in &mut chunks { - /// let value = S::u32s::from_slice(simd, chunk); - /// (value * 2).store_slice(chunk); - /// } - /// for value in chunks.into_remainder() { - /// *value *= 2; - /// } - /// }, + /// || double_u32s_inner(simd, values), /// ); /// } /// @@ -187,27 +190,24 @@ pub(crate) fn mk_simd_trait() -> TokenStream { /// ``` fn vectorize R, R>(self, f: F) -> R; - /// Call function with SIMD instructions enabled, without forcing [inlining](https://matklad.github.io/2021/07/09/inline-in-rust.html). + /// Call function with SIMD instructions enabled. /// - /// `vectorize()` will set the correct `#[target_feature]` annotations for the SIMD level. + /// This is useful for creating SIMD functions that do not have to be annotated `#[inline(always)]`. + /// + /// `vectorize_inline()` will set the correct `#[target_feature]` annotations for the SIMD level. /// The provided function should be `#[inline(always)]`, otherwise it may not /// be able to utilize the best SIMD instructions available. - /// `vectorize()` itself acts as the function boundary in machine code. - /// - /// This is useful when the SIMD implementation has already been selected and you want - /// to keep a SIMD-generic function outlined instead of forcing the entire function to - /// be inlined into its caller. /// /// # Example /// /// `double_u32s` is deliberately not marked `#[inline(always)]`. Instead, only its - /// closure is inlined into the target-feature-enabled boundary created by `vectorize()`. + /// closure is inlined into the target-feature-enabled context created by `vectorize_inline()`. /// /// ``` /// use fearless_simd::{dispatch, prelude::*, Level}; /// /// fn double_u32s(simd: S, values: &mut [u32]) { - /// simd.vectorize( + /// simd.vectorize_inline( /// #[inline(always)] /// || { /// let mut chunks = values.chunks_exact_mut(S::u32s::N); From 21c94cc7ace8e7f955482c05ada7916f619996ff Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Mon, 24 Aug 2026 16:31:11 +0100 Subject: [PATCH 04/20] Revert vectorize_inline changes --- fearless_simd/src/generated/avx2.rs | 11 ----- fearless_simd/src/generated/avx512.rs | 11 ----- fearless_simd/src/generated/fallback.rs | 8 ---- fearless_simd/src/generated/neon.rs | 9 ---- fearless_simd/src/generated/simd_trait.rs | 55 +++------------------- fearless_simd/src/generated/sse2.rs | 9 ---- fearless_simd/src/generated/sse4_2.rs | 9 ---- fearless_simd/src/generated/wasm.rs | 8 ---- fearless_simd_gen/src/level.rs | 26 ----------- fearless_simd_gen/src/mk_simd_trait.rs | 56 +++-------------------- 10 files changed, 14 insertions(+), 188 deletions(-) diff --git a/fearless_simd/src/generated/avx2.rs b/fearless_simd/src/generated/avx2.rs index cfa9ef83..bad30bd8 100644 --- a/fearless_simd/src/generated/avx2.rs +++ b/fearless_simd/src/generated/avx2.rs @@ -116,17 +116,6 @@ impl Simd for Avx2 { unsafe { vectorize_avx2(f) } } #[inline(always)] - fn vectorize_inline R, R>(self, f: F) -> R { - #[inline] - #[target_feature( - enable = "avx2,bmi1,bmi2,cmpxchg16b,f16c,fma,fxsr,lzcnt,movbe,popcnt,xsave" - )] - fn vectorize_inline_avx2 R, R>(f: F) -> R { - f() - } - unsafe { vectorize_inline_avx2(f) } - } - #[inline(always)] fn splat_f32x4(self, val: f32) -> f32x4 { crate::kernel!( #[inline(always)] diff --git a/fearless_simd/src/generated/avx512.rs b/fearless_simd/src/generated/avx512.rs index 6f84c93e..437a157e 100644 --- a/fearless_simd/src/generated/avx512.rs +++ b/fearless_simd/src/generated/avx512.rs @@ -390,17 +390,6 @@ impl Simd for Avx512 { unsafe { vectorize_avx512(f) } } #[inline(always)] - fn vectorize_inline R, R>(self, f: F) -> R { - #[inline] - #[target_feature( - enable = "adx,aes,avx512bitalg,avx512bw,avx512cd,avx512dq,avx512f,avx512ifma,avx512vbmi,avx512vbmi2,avx512vl,avx512vnni,avx512vpopcntdq,bmi1,bmi2,cmpxchg16b,fma,fxsr,gfni,lzcnt,movbe,pclmulqdq,popcnt,rdrand,rdseed,sha,vaes,vpclmulqdq,xsave,xsavec,xsaveopt,xsaves" - )] - fn vectorize_inline_avx512 R, R>(f: F) -> R { - f() - } - unsafe { vectorize_inline_avx512(f) } - } - #[inline(always)] fn splat_f32x4(self, val: f32) -> f32x4 { crate::kernel!( #[inline(always)] diff --git a/fearless_simd/src/generated/fallback.rs b/fearless_simd/src/generated/fallback.rs index 1e405d4f..fb46ebf4 100644 --- a/fearless_simd/src/generated/fallback.rs +++ b/fearless_simd/src/generated/fallback.rs @@ -198,14 +198,6 @@ impl Simd for Fallback { vectorize_inner(f) } #[inline(always)] - fn vectorize_inline R, R>(self, f: F) -> R { - #[inline] - fn vectorize_inline_inner R, R>(f: F) -> R { - f() - } - vectorize_inline_inner(f) - } - #[inline(always)] fn splat_f32x4(self, val: f32) -> f32x4 { [val; 4usize].simd_into(self) } diff --git a/fearless_simd/src/generated/neon.rs b/fearless_simd/src/generated/neon.rs index 6d8cc36f..e4418186 100644 --- a/fearless_simd/src/generated/neon.rs +++ b/fearless_simd/src/generated/neon.rs @@ -109,15 +109,6 @@ impl Simd for Neon { unsafe { vectorize_neon(f) } } #[inline(always)] - fn vectorize_inline R, R>(self, f: F) -> R { - #[inline] - #[target_feature(enable = "neon")] - fn vectorize_inline_neon R, R>(f: F) -> R { - f() - } - unsafe { vectorize_inline_neon(f) } - } - #[inline(always)] fn splat_f32x4(self, val: f32) -> f32x4 { crate::kernel!( #[inline(always)] diff --git a/fearless_simd/src/generated/simd_trait.rs b/fearless_simd/src/generated/simd_trait.rs index 89c97d88..f1ce7214 100644 --- a/fearless_simd/src/generated/simd_trait.rs +++ b/fearless_simd/src/generated/simd_trait.rs @@ -174,66 +174,25 @@ pub trait Simd: fn level(self) -> Level; #[doc = r" Call function with SIMD instructions enabled, without forcing [inlining](https://matklad.github.io/2021/07/09/inline-in-rust.html)."] #[doc = r""] - #[doc = r" This is useful for calling SIMD functions annotated `#[inline(always)]`"] - #[doc = r" when you don't want to actually force inlining all the way up to a `dispatch!` call."] - #[doc = r" `vectorize()` acts as the function boundary in machine code."] - #[doc = r""] #[doc = r" `vectorize()` will set the correct `#[target_feature]` annotations for the SIMD level."] #[doc = r" The provided function should be `#[inline(always)]`, otherwise it may not"] #[doc = r" be able to utilize the best SIMD instructions available."] + #[doc = r" `vectorize()` itself acts as the function boundary in machine code."] #[doc = r""] - #[doc = r" # Example"] - #[doc = r""] - #[doc = r" `double_u32s_inner` is marked `#[inline(always)]`, but `double_u32s` is not."] - #[doc = r" `vectorize()` creates a function boundary so the implementation does not have to be"] - #[doc = r" inlined all the way into the `dispatch!` call."] - #[doc = r""] - #[doc = r" ```"] - #[doc = r" use fearless_simd::{dispatch, prelude::*, Level};"] - #[doc = r""] - #[doc = r" #[inline(always)]"] - #[doc = r" fn double_u32s_inner(simd: S, values: &mut [u32]) {"] - #[doc = r" let mut chunks = values.chunks_exact_mut(S::u32s::N);"] - #[doc = r" for chunk in &mut chunks {"] - #[doc = r" let value = S::u32s::from_slice(simd, chunk);"] - #[doc = r" (value * 2).store_slice(chunk);"] - #[doc = r" }"] - #[doc = r" for value in chunks.into_remainder() {"] - #[doc = r" *value *= 2;"] - #[doc = r" }"] - #[doc = r" }"] - #[doc = r""] - #[doc = r" fn double_u32s(simd: S, values: &mut [u32]) {"] - #[doc = r" simd.vectorize("] - #[doc = r" #[inline(always)]"] - #[doc = r" || double_u32s_inner(simd, values),"] - #[doc = r" );"] - #[doc = r" }"] - #[doc = r""] - #[doc = r" let mut values = [1, 2, 3, 4, 5];"] - #[doc = r" let level = Level::new();"] - #[doc = r" dispatch!(level, simd => double_u32s(simd, &mut values));"] - #[doc = r" assert_eq!(values, [2, 4, 6, 8, 10]);"] - #[doc = r" ```"] - fn vectorize R, R>(self, f: F) -> R; - #[doc = r" Call function with SIMD instructions enabled."] - #[doc = r""] - #[doc = r" This is useful for creating SIMD functions that do not have to be annotated `#[inline(always)]`."] - #[doc = r""] - #[doc = r" `vectorize_inline()` will set the correct `#[target_feature]` annotations for the SIMD level."] - #[doc = r" The provided function should be `#[inline(always)]`, otherwise it may not"] - #[doc = r" be able to utilize the best SIMD instructions available."] + #[doc = r" This is useful when the SIMD implementation has already been selected and you want"] + #[doc = r" to keep a SIMD-generic function outlined instead of forcing the entire function to"] + #[doc = r" be inlined into its caller."] #[doc = r""] #[doc = r" # Example"] #[doc = r""] #[doc = r" `double_u32s` is deliberately not marked `#[inline(always)]`. Instead, only its"] - #[doc = r" closure is inlined into the target-feature-enabled context created by `vectorize_inline()`."] + #[doc = r" closure is inlined into the target-feature-enabled boundary created by `vectorize()`."] #[doc = r""] #[doc = r" ```"] #[doc = r" use fearless_simd::{dispatch, prelude::*, Level};"] #[doc = r""] #[doc = r" fn double_u32s(simd: S, values: &mut [u32]) {"] - #[doc = r" simd.vectorize_inline("] + #[doc = r" simd.vectorize("] #[doc = r" #[inline(always)]"] #[doc = r" || {"] #[doc = r" let mut chunks = values.chunks_exact_mut(S::u32s::N);"] @@ -253,7 +212,7 @@ pub trait Simd: #[doc = r" dispatch!(level, simd => double_u32s(simd, &mut values));"] #[doc = r" assert_eq!(values, [2, 4, 6, 8, 10]);"] #[doc = r" ```"] - fn vectorize_inline R, R>(self, f: F) -> R; + fn vectorize R, R>(self, f: F) -> R; #[doc = "Create a SIMD vector with all elements set to the given value."] fn splat_f32x4(self, val: f32) -> f32x4; #[doc = "Reverse the order of the vector's elements."] diff --git a/fearless_simd/src/generated/sse2.rs b/fearless_simd/src/generated/sse2.rs index f7349f93..6ea22fef 100644 --- a/fearless_simd/src/generated/sse2.rs +++ b/fearless_simd/src/generated/sse2.rs @@ -212,15 +212,6 @@ impl Simd for Sse2 { unsafe { vectorize_sse2(f) } } #[inline(always)] - fn vectorize_inline R, R>(self, f: F) -> R { - #[inline] - #[target_feature(enable = "fxsr,sse,sse2")] - fn vectorize_inline_sse2 R, R>(f: F) -> R { - f() - } - unsafe { vectorize_inline_sse2(f) } - } - #[inline(always)] fn splat_f32x4(self, val: f32) -> f32x4 { crate::kernel!( #[inline(always)] diff --git a/fearless_simd/src/generated/sse4_2.rs b/fearless_simd/src/generated/sse4_2.rs index f2055919..87b7e7c1 100644 --- a/fearless_simd/src/generated/sse4_2.rs +++ b/fearless_simd/src/generated/sse4_2.rs @@ -190,15 +190,6 @@ impl Simd for Sse4_2 { unsafe { vectorize_sse4_2(f) } } #[inline(always)] - fn vectorize_inline R, R>(self, f: F) -> R { - #[inline] - #[target_feature(enable = "fxsr,sse4.2,cmpxchg16b,popcnt")] - fn vectorize_inline_sse4_2 R, R>(f: F) -> R { - f() - } - unsafe { vectorize_inline_sse4_2(f) } - } - #[inline(always)] fn splat_f32x4(self, val: f32) -> f32x4 { crate::kernel!( #[inline(always)] diff --git a/fearless_simd/src/generated/wasm.rs b/fearless_simd/src/generated/wasm.rs index 03a64bb5..5ce55edd 100644 --- a/fearless_simd/src/generated/wasm.rs +++ b/fearless_simd/src/generated/wasm.rs @@ -200,14 +200,6 @@ impl Simd for WasmSimd128 { vectorize_inner(f) } #[inline(always)] - fn vectorize_inline R, R>(self, f: F) -> R { - #[inline] - fn vectorize_inline_inner R, R>(f: F) -> R { - f() - } - vectorize_inline_inner(f) - } - #[inline(always)] fn splat_f32x4(self, val: f32) -> f32x4 { f32x4_splat(val).simd_into(self) } diff --git a/fearless_simd_gen/src/level.rs b/fearless_simd_gen/src/level.rs index 3a2b3baf..8acc16c9 100644 --- a/fearless_simd_gen/src/level.rs +++ b/fearless_simd_gen/src/level.rs @@ -175,27 +175,6 @@ pub(crate) trait Level { } }; - let vectorize_inline_body = if let Some(target_features) = self.enabled_target_features() { - let vectorize_inline = - format_ident!("vectorize_inline_{}", self.name().to_ascii_lowercase()); - quote! { - #[inline] - #[target_feature(enable = #target_features)] - fn #vectorize_inline R, R>(f: F) -> R { - f() - } - unsafe { #vectorize_inline(f) } - } - } else { - quote! { - #[inline] - fn vectorize_inline_inner R, R>(f: F) -> R { - f() - } - vectorize_inline_inner(f) - } - }; - let level_body = self.make_level_body(); let mut assoc_types = vec![]; @@ -237,11 +216,6 @@ pub(crate) trait Level { #vectorize_body } - #[inline(always)] - fn vectorize_inline R, R>(self, f: F) -> R { - #vectorize_inline_body - } - #( #[inline(always)] #methods diff --git a/fearless_simd_gen/src/mk_simd_trait.rs b/fearless_simd_gen/src/mk_simd_trait.rs index e02cbb49..5cd38705 100644 --- a/fearless_simd_gen/src/mk_simd_trait.rs +++ b/fearless_simd_gen/src/mk_simd_trait.rs @@ -147,67 +147,25 @@ pub(crate) fn mk_simd_trait() -> TokenStream { /// Call function with SIMD instructions enabled, without forcing [inlining](https://matklad.github.io/2021/07/09/inline-in-rust.html). /// - /// This is useful for calling SIMD functions annotated `#[inline(always)]` - /// when you don't want to actually force inlining all the way up to a `dispatch!` call. - /// `vectorize()` acts as the function boundary in machine code. - /// /// `vectorize()` will set the correct `#[target_feature]` annotations for the SIMD level. /// The provided function should be `#[inline(always)]`, otherwise it may not /// be able to utilize the best SIMD instructions available. + /// `vectorize()` itself acts as the function boundary in machine code. /// - /// # Example - /// - /// `double_u32s_inner` is marked `#[inline(always)]`, but `double_u32s` is not. - /// `vectorize()` creates a function boundary so the implementation does not have to be - /// inlined all the way into the `dispatch!` call. - /// - /// ``` - /// use fearless_simd::{dispatch, prelude::*, Level}; - /// - /// #[inline(always)] - /// fn double_u32s_inner(simd: S, values: &mut [u32]) { - /// let mut chunks = values.chunks_exact_mut(S::u32s::N); - /// for chunk in &mut chunks { - /// let value = S::u32s::from_slice(simd, chunk); - /// (value * 2).store_slice(chunk); - /// } - /// for value in chunks.into_remainder() { - /// *value *= 2; - /// } - /// } - /// - /// fn double_u32s(simd: S, values: &mut [u32]) { - /// simd.vectorize( - /// #[inline(always)] - /// || double_u32s_inner(simd, values), - /// ); - /// } - /// - /// let mut values = [1, 2, 3, 4, 5]; - /// let level = Level::new(); - /// dispatch!(level, simd => double_u32s(simd, &mut values)); - /// assert_eq!(values, [2, 4, 6, 8, 10]); - /// ``` - fn vectorize R, R>(self, f: F) -> R; - - /// Call function with SIMD instructions enabled. - /// - /// This is useful for creating SIMD functions that do not have to be annotated `#[inline(always)]`. - /// - /// `vectorize_inline()` will set the correct `#[target_feature]` annotations for the SIMD level. - /// The provided function should be `#[inline(always)]`, otherwise it may not - /// be able to utilize the best SIMD instructions available. + /// This is useful when the SIMD implementation has already been selected and you want + /// to keep a SIMD-generic function outlined instead of forcing the entire function to + /// be inlined into its caller. /// /// # Example /// /// `double_u32s` is deliberately not marked `#[inline(always)]`. Instead, only its - /// closure is inlined into the target-feature-enabled context created by `vectorize_inline()`. + /// closure is inlined into the target-feature-enabled boundary created by `vectorize()`. /// /// ``` /// use fearless_simd::{dispatch, prelude::*, Level}; /// /// fn double_u32s(simd: S, values: &mut [u32]) { - /// simd.vectorize_inline( + /// simd.vectorize( /// #[inline(always)] /// || { /// let mut chunks = values.chunks_exact_mut(S::u32s::N); @@ -227,7 +185,7 @@ pub(crate) fn mk_simd_trait() -> TokenStream { /// dispatch!(level, simd => double_u32s(simd, &mut values)); /// assert_eq!(values, [2, 4, 6, 8, 10]); /// ``` - fn vectorize_inline R, R>(self, f: F) -> R; + fn vectorize R, R>(self, f: F) -> R; #( #methods )* } }; From 98bea972509bfd8bb48d2020b27b26a893ecea88 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Mon, 24 Aug 2026 16:44:57 +0100 Subject: [PATCH 05/20] mark the inner function under vectorize() as #[inline] to better match the documented use case for vectorize(). Does not affect usage in dispatch!() which dispatches from non-target-feature context. The function itself was already trivial and under -O3 the optimizer would recognize it as trivial and inline it anyway, so adding a function boundary there didn't really work as intended, except under -Os. --- fearless_simd/src/generated/avx2.rs | 1 + fearless_simd/src/generated/avx512.rs | 1 + fearless_simd/src/generated/neon.rs | 1 + fearless_simd/src/generated/sse2.rs | 1 + fearless_simd/src/generated/sse4_2.rs | 1 + fearless_simd_gen/src/level.rs | 4 +--- 6 files changed, 6 insertions(+), 3 deletions(-) diff --git a/fearless_simd/src/generated/avx2.rs b/fearless_simd/src/generated/avx2.rs index bad30bd8..1f01ab8e 100644 --- a/fearless_simd/src/generated/avx2.rs +++ b/fearless_simd/src/generated/avx2.rs @@ -107,6 +107,7 @@ impl Simd for Avx2 { } #[inline(always)] fn vectorize R, R>(self, f: F) -> R { + #[inline] #[target_feature( enable = "avx2,bmi1,bmi2,cmpxchg16b,f16c,fma,fxsr,lzcnt,movbe,popcnt,xsave" )] diff --git a/fearless_simd/src/generated/avx512.rs b/fearless_simd/src/generated/avx512.rs index 437a157e..05daf872 100644 --- a/fearless_simd/src/generated/avx512.rs +++ b/fearless_simd/src/generated/avx512.rs @@ -381,6 +381,7 @@ impl Simd for Avx512 { } #[inline(always)] fn vectorize R, R>(self, f: F) -> R { + #[inline] #[target_feature( enable = "adx,aes,avx512bitalg,avx512bw,avx512cd,avx512dq,avx512f,avx512ifma,avx512vbmi,avx512vbmi2,avx512vl,avx512vnni,avx512vpopcntdq,bmi1,bmi2,cmpxchg16b,fma,fxsr,gfni,lzcnt,movbe,pclmulqdq,popcnt,rdrand,rdseed,sha,vaes,vpclmulqdq,xsave,xsavec,xsaveopt,xsaves" )] diff --git a/fearless_simd/src/generated/neon.rs b/fearless_simd/src/generated/neon.rs index e4418186..ec8ae7ed 100644 --- a/fearless_simd/src/generated/neon.rs +++ b/fearless_simd/src/generated/neon.rs @@ -102,6 +102,7 @@ impl Simd for Neon { } #[inline(always)] fn vectorize R, R>(self, f: F) -> R { + #[inline] #[target_feature(enable = "neon")] fn vectorize_neon R, R>(f: F) -> R { f() diff --git a/fearless_simd/src/generated/sse2.rs b/fearless_simd/src/generated/sse2.rs index 6ea22fef..6551417e 100644 --- a/fearless_simd/src/generated/sse2.rs +++ b/fearless_simd/src/generated/sse2.rs @@ -205,6 +205,7 @@ impl Simd for Sse2 { } #[inline(always)] fn vectorize R, R>(self, f: F) -> R { + #[inline] #[target_feature(enable = "fxsr,sse,sse2")] fn vectorize_sse2 R, R>(f: F) -> R { f() diff --git a/fearless_simd/src/generated/sse4_2.rs b/fearless_simd/src/generated/sse4_2.rs index 87b7e7c1..2c7b661b 100644 --- a/fearless_simd/src/generated/sse4_2.rs +++ b/fearless_simd/src/generated/sse4_2.rs @@ -183,6 +183,7 @@ impl Simd for Sse4_2 { } #[inline(always)] fn vectorize R, R>(self, f: F) -> R { + #[inline] #[target_feature(enable = "fxsr,sse4.2,cmpxchg16b,popcnt")] fn vectorize_sse4_2 R, R>(f: F) -> R { f() diff --git a/fearless_simd_gen/src/level.rs b/fearless_simd_gen/src/level.rs index 8acc16c9..84a6ecc8 100644 --- a/fearless_simd_gen/src/level.rs +++ b/fearless_simd_gen/src/level.rs @@ -152,9 +152,7 @@ pub(crate) trait Level { let vectorize_body = if let Some(target_features) = self.enabled_target_features() { let vectorize = format_ident!("vectorize_{}", self.name().to_ascii_lowercase()); quote! { - // This function is deliberately not marked #[inline]: - // The closure passed to it is already required to be #[inline(always)], - // so this wrapper is the only opportunity for the compiler to make inlining decisions. + #[inline] #[target_feature(enable = #target_features)] fn #vectorize R, R>(f: F) -> R { f() From 5660989dff97234116e7a31317cfc58e4ae28365 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Mon, 24 Aug 2026 17:42:31 +0100 Subject: [PATCH 06/20] Add standalone simd attribute macro --- .github/workflows/ci.yml | 9 +- CHANGELOG.md | 2 + Cargo.lock | 207 +++++++- Cargo.toml | 11 +- README.md | 15 +- fearless_simd/README.md | 52 +- fearless_simd/src/generated/fallback.rs | 1 + fearless_simd/src/generated/simd_trait.rs | 24 +- fearless_simd/src/generated/wasm.rs | 1 + fearless_simd/src/lib.rs | 56 +- fearless_simd/src/macros.rs | 7 +- fearless_simd_gen/src/level.rs | 8 +- fearless_simd_gen/src/mk_simd_trait.rs | 24 +- fearless_simd_macros/Cargo.toml | 23 + fearless_simd_macros/LICENSE-APACHE | 176 ++++++ fearless_simd_macros/LICENSE-MIT | 25 + fearless_simd_macros/README.md | 116 ++++ fearless_simd_macros/src/lib.rs | 500 ++++++++++++++++++ fearless_simd_tests/Cargo.toml | 14 +- fearless_simd_tests/src/lib.rs | 20 + fearless_simd_tests/tests/mod.rs | 1 + fearless_simd_tests/tests/simd_macro.rs | 98 ++++ fearless_simd_tests/tests/simd_ui.rs | 13 + .../tests/ui/simd/fail/arguments.rs | 11 + .../tests/ui/simd/fail/arguments.stderr | 5 + .../tests/ui/simd/fail/async.rs | 11 + .../tests/ui/simd/fail/async.stderr | 5 + .../tests/ui/simd/fail/at_token.rs | 11 + .../tests/ui/simd/fail/at_token.stderr | 5 + .../tests/ui/simd/fail/bodyless.rs | 11 + .../tests/ui/simd/fail/bodyless.stderr | 5 + .../tests/ui/simd/fail/const.rs | 11 + .../tests/ui/simd/fail/const.stderr | 5 + .../tests/ui/simd/fail/default_modifier.rs | 21 + .../ui/simd/fail/default_modifier.stderr | 5 + .../tests/ui/simd/fail/destructured_token.rs | 11 + .../ui/simd/fail/destructured_token.stderr | 5 + .../tests/ui/simd/fail/instruction_set.rs | 12 + .../tests/ui/simd/fail/instruction_set.stderr | 5 + .../tests/ui/simd/fail/naked.rs | 12 + .../tests/ui/simd/fail/naked.stderr | 5 + .../tests/ui/simd/fail/no_token.rs | 9 + .../tests/ui/simd/fail/no_token.stderr | 7 + .../tests/ui/simd/fail/non_function.rs | 9 + .../tests/ui/simd/fail/non_function.stderr | 5 + .../tests/ui/simd/fail/parameter_cfg.rs | 11 + .../tests/ui/simd/fail/parameter_cfg.stderr | 5 + .../tests/ui/simd/fail/ref_token.rs | 11 + .../tests/ui/simd/fail/ref_token.stderr | 5 + .../tests/ui/simd/fail/track_caller.rs | 12 + .../tests/ui/simd/fail/track_caller.stderr | 5 + .../tests/ui/simd/fail/variadic.rs | 11 + .../tests/ui/simd/fail/variadic.stderr | 5 + .../tests/ui/simd/pass/functions.rs | 74 +++ .../tests/ui/simd/pass/methods.rs | 42 ++ 55 files changed, 1715 insertions(+), 55 deletions(-) create mode 100644 fearless_simd_macros/Cargo.toml create mode 100644 fearless_simd_macros/LICENSE-APACHE create mode 100644 fearless_simd_macros/LICENSE-MIT create mode 100644 fearless_simd_macros/README.md create mode 100644 fearless_simd_macros/src/lib.rs create mode 100644 fearless_simd_tests/src/lib.rs create mode 100644 fearless_simd_tests/tests/simd_macro.rs create mode 100644 fearless_simd_tests/tests/simd_ui.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/arguments.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/arguments.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/async.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/async.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/at_token.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/at_token.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/bodyless.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/bodyless.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/const.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/const.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/default_modifier.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/default_modifier.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/destructured_token.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/destructured_token.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/instruction_set.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/instruction_set.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/naked.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/naked.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/no_token.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/no_token.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/non_function.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/non_function.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/ref_token.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/ref_token.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/track_caller.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/track_caller.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/variadic.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/variadic.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/pass/functions.rs create mode 100644 fearless_simd_tests/tests/ui/simd/pass/methods.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49da3c0a..0fe54ec7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ env: RUST_MIN_VER: "1.89" # List of packages that will be checked with the minimum supported Rust version. # This should be limited to packages that are intended for publishing. - RUST_MIN_VER_PKGS: "-p fearless_simd" + RUST_MIN_VER_PKGS: "-p fearless_simd -p fearless_simd_macros" # List of features that depend on the standard library and will be excluded from no_std checks. FEATURES_DEPENDING_ON_STD: "std,default" # List of packages that can not target Wasm. @@ -128,6 +128,9 @@ jobs: - name: cargo clippy (no_std) run: cargo hack clippy -p fearless_simd --locked --optional-deps --each-feature --ignore-unknown-features --features libm --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} --target x86_64-unknown-none -- -D warnings + - name: cargo clippy (simd macro in no_std) + run: cargo clippy -p fearless_simd_tests --lib --locked --target x86_64-unknown-none --no-default-features -- -D warnings + - name: cargo clippy run: cargo hack clippy --workspace --locked --optional-deps --each-feature --ignore-unknown-features --features std -- -D warnings @@ -219,6 +222,10 @@ jobs: - name: cargo test run: cargo test --workspace --locked --all-features --no-fail-fast --target ${{ matrix.platform.target }} + - name: cargo test (simd macro UI) + if: matrix.platform.target == 'x86_64-unknown-linux-gnu' + run: cargo test -p fearless_simd_tests --locked --test simd_ui --target ${{ matrix.platform.target }} -- --ignored + test-sde: name: cargo test in an emulator runs-on: ${{ matrix.platform.os }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 08f0e187..2bf184ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,10 +18,12 @@ You can find its changes [documented below](#070-2026-08-11). - Added `mul_add_precise` and `mul_sub_precise` for floating-point vectors. They guarantee the infinite-precision product-plus-add rounded once, including on SIMD levels without hardware fused multiply-add instructions. They are not susceptible to the [bug](https://github.com/rust-lang/compiler-builtins/issues/1262) in Rust standard library, `std::simd` and musl libc that causes incorrect rounding for subnormal results. SSE4.2 gets SIMD emulation of these operations for better performance. ([#323][], [#324][] by [@Shnatsel][]) - Documented the storage representation of the SIMD vector types. The documented representation will not change without a semver major version change. - Added `TryFrom` bounds to `SimdIntElement`, allowing attempted conversion from all primitive integer types. +- Added the separately versioned, experimental `fearless_simd_macros` companion crate. Its `#[simd]` attribute wraps SIMD-generic function bodies using the public `Simd::vectorize` API, while the core `fearless_simd` crate remains dependency-free. ### Changed - Breaking change: `SimdBase::as_array` now borrows the vector and returns an array reference, while owned extraction has moved to `to_array`. The old `as_array_ref` and `as_array_mut` methods have been replaced by `as_array` and `as_mut_array`, matching the `std::simd` API. +- `Simd::vectorize` is now inline-friendly on every backend. It retains a target-feature boundary when one is required, but no longer attempts to guarantee an outlined machine-code boundary. Documentation now recommends the optional `#[simd]` attribute for ergonomics and shows direct `vectorize` calls as the dependency-free equivalent. ## [0.7.0][] (2026-08-11) diff --git a/Cargo.lock b/Cargo.lock index 142c3562..54019df2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,7 +89,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.103", ] [[package]] @@ -104,6 +104,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "fastrand" version = "2.5.0" @@ -123,7 +129,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -136,6 +142,15 @@ dependencies = [ "quote", ] +[[package]] +name = "fearless_simd_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + [[package]] name = "fearless_simd_tests" version = "0.0.0" @@ -143,26 +158,62 @@ dependencies = [ "fastrand", "fearless_simd", "fearless_simd_dev_macros", + "fearless_simd_macros", + "trybuild", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "libm" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + [[package]] name = "once_cell_polyfill" version = "1.70.1" @@ -187,6 +238,57 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "strsim" version = "0.11.1" @@ -204,6 +306,86 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-triple" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "trybuild" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -216,6 +398,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -288,3 +479,15 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 7b94f86a..e5350d1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "fearless_simd", "fearless_simd_dev_macros", "fearless_simd_gen", + "fearless_simd_macros", "fearless_simd_tests", ] @@ -76,8 +77,14 @@ clippy.wildcard_dependencies = "warn" clippy.uninlined_format_args = "allow" [workspace.dependencies] -fearless_simd = { path = "fearless_simd" } +fearless_simd = { path = "fearless_simd", default-features = false } fearless_simd_dev_macros = { path = "fearless_simd_dev_macros" } +fearless_simd_macros = { path = "fearless_simd_macros", version = "0.1.0" } proc-macro2 = "1.0.95" -syn = { version = "2.0.101", features = ["full", "extra-traits"] } +syn = { version = "3.0.4", default-features = false, features = [ + "full", + "parsing", + "printing", + "proc-macro", +] } quote = "1.0.40" diff --git a/README.md b/README.md index 05667e4f..4275b39a 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,11 @@ For now, prefer updating the package-level readmes, e.g. fearless_simd/README.md -Full details of how to use Fearless SIMD can be found in the [Fearless SIMD package's README](./fearless_simd/README.md). +Full details of how to use Fearless SIMD can be found in the +[core package's README](./fearless_simd/README.md). The optional, experimental `#[simd]` +attribute is provided by the separately versioned +[`fearless_simd_macros` companion package](./fearless_simd_macros/README.md); the core package does +not depend on it. ## Motivation @@ -33,9 +37,12 @@ The blog post [Safe SIMD in Rust, even on the inside] describes the design and h ## Repository Structure -The only package which is published to crates.io from this repository is Fearless SIMD, which can be found in the `fearless_simd` folder. -This folder also contains the examples. -The other packages are as follows: +This repository contains two packages published to crates.io: + +- `fearless_simd`: The zero-dependency core library, including the examples. +- `fearless_simd_macros`: A separately versioned, experimental companion crate providing the optional `#[simd]` procedural macro. + +The remaining packages are development tools and tests: - `fearless_simd_gen`: A code generator, used to generate the low signal-to-noise parts of the Fearless SIMD crate. - `fearless_simd_tests`: Tests of functionality in Fearless SIMD, to validate that all implementations give the same and correct results. diff --git a/fearless_simd/README.md b/fearless_simd/README.md index f07d3038..f4436663 100644 --- a/fearless_simd/README.md +++ b/fearless_simd/README.md @@ -42,18 +42,26 @@ See https://linebender.org/blog/doc-include/ for related discussion. --> No matter what level of abstraction you're after, be it autovectorization and multiversioning, or portable SIMD, or safe access to raw intrinsics and nothing more, `fearless_simd` has you covered! -Zero dependencies, safe public APIs, and [very little](https://shnatsel.github.io/safe-simd-in-rust-even-on-the-inside/) `unsafe` under the hood. +The core crate has zero dependencies, safe public APIs, and +[very little](https://shnatsel.github.io/safe-simd-in-rust-even-on-the-inside/) +`unsafe` under the hood. ## Automatic vectorization -Put the code to vectorize in an `#[inline(always)]` function generic over [`Simd`]. +The easiest way to define a SIMD-generic function is the experimental +[`#[simd]`](https://docs.rs/fearless_simd_macros/latest/fearless_simd_macros/attr.simd.html) +attribute from the separately versioned `fearless_simd_macros` crate. The companion macro +crate is optional: `fearless_simd` does not depend on it, so users of only the core API do not +pay for its procedural-macro dependencies. -This will generate several implementations for different SIMD levels and select the best one at runtime: +[`dispatch`] generates implementations for the available SIMD levels and selects the best one +at runtime: ```rust use fearless_simd::{dispatch, Level, Simd}; +use fearless_simd_macros::simd; -#[inline(always)] +#[simd] fn double_u32s(_: S, values: &mut [u32]) { for value in values { *value = *value * 2; @@ -66,14 +74,38 @@ dispatch!(level, simd => double_u32s(simd, &mut values)); assert_eq!(values, [2, 4, 6, 8, 10]); ``` +The attribute uses the public [`Simd::vectorize`] API. You can call it directly to get the same +behavior without depending on the macro crate: + +```rust +use fearless_simd::{dispatch, Level, Simd}; + +fn double_u32s(simd: S, values: &mut [u32]) { + simd.vectorize( + #[inline(always)] + || { + for value in values { + *value = *value * 2; + } + }, + ); +} + +let mut values = [1, 2, 3, 4, 5]; +let level = Level::new(); +dispatch!(level, simd => double_u32s(simd, &mut values)); +assert_eq!(values, [2, 4, 6, 8, 10]); +``` + ## Portable SIMD Use the vector types for explicit lane-wise operations while staying generic over the SIMD level: ```rust use fearless_simd::{dispatch, prelude::*, Level}; +use fearless_simd_macros::simd; -#[inline(always)] +#[simd] fn double_u32s(simd: S, values: &mut [u32]) { let mut chunks = values.chunks_exact_mut(S::u32s::N); // the CPU's native SIMD width for chunk in &mut chunks { @@ -134,13 +166,17 @@ hardware-specific intrinsics only when necessary. ## Inlining -Fearless SIMD relies heavily on Rust's inlining support to create functions which have the given target features enabled. +Fearless SIMD relies on Rust's inlining support to place SIMD code in a context with the +appropriate target features enabled. As a rule of thumb: -- All SIMD functions need `#[inline(always)]`. +- Put `#[simd]` on SIMD-generic functions, or wrap their bodies in [`Simd::vectorize`]. - Use [`dispatch`] when calling SIMD code from non-SIMD code. -- Use [`vectorize()`][Simd::vectorize] when calling SIMD from SIMD if you don't want to force inlining. +- A closure passed directly to [`Simd::vectorize`] needs `#[inline(always)]`; `#[simd]` adds + this closure annotation for you. +- Helpers that are not themselves wrapped by `#[simd]` or [`Simd::vectorize`] still need to be + inlined into a SIMD-enabled caller. [The article describing the design](https://shnatsel.github.io/safe-simd-in-rust-even-on-the-inside/#the-abi-would-like-a-word) covers why this is the case. There's also Q&A on [Zulip](https://xi.zulipchat.com/#narrow/channel/514230-simd/topic/inlining/with/546913433). diff --git a/fearless_simd/src/generated/fallback.rs b/fearless_simd/src/generated/fallback.rs index fb46ebf4..04a612fa 100644 --- a/fearless_simd/src/generated/fallback.rs +++ b/fearless_simd/src/generated/fallback.rs @@ -192,6 +192,7 @@ impl Simd for Fallback { } #[inline(always)] fn vectorize R, R>(self, f: F) -> R { + #[inline] fn vectorize_inner R, R>(f: F) -> R { f() } diff --git a/fearless_simd/src/generated/simd_trait.rs b/fearless_simd/src/generated/simd_trait.rs index f1ce7214..314022ab 100644 --- a/fearless_simd/src/generated/simd_trait.rs +++ b/fearless_simd/src/generated/simd_trait.rs @@ -172,21 +172,25 @@ pub trait Simd: + Select; #[doc = r" This SIMD token's feature level."] fn level(self) -> Level; - #[doc = r" Call function with SIMD instructions enabled, without forcing [inlining](https://matklad.github.io/2021/07/09/inline-in-rust.html)."] + #[doc = r" Call a closure with the instructions for this SIMD level enabled."] #[doc = r""] - #[doc = r" `vectorize()` will set the correct `#[target_feature]` annotations for the SIMD level."] - #[doc = r" The provided function should be `#[inline(always)]`, otherwise it may not"] - #[doc = r" be able to utilize the best SIMD instructions available."] - #[doc = r" `vectorize()` itself acts as the function boundary in machine code."] + #[doc = r" `vectorize()` establishes the correct `#[target_feature]` context. The provided"] + #[doc = r" closure should be `#[inline(always)]` so that its body is incorporated into that"] + #[doc = r" context and can use all instructions supported by this SIMD level."] #[doc = r""] - #[doc = r" This is useful when the SIMD implementation has already been selected and you want"] - #[doc = r" to keep a SIMD-generic function outlined instead of forcing the entire function to"] - #[doc = r" be inlined into its caller."] + #[doc = r" The small wrappers used by `vectorize()` are eligible for inlining when the caller"] + #[doc = r" has a compatible target-feature context. A function containing the call does not"] + #[doc = r" itself need an `#[inline(always)]` annotation; a target-feature boundary is retained"] + #[doc = r" whenever one is required."] + #[doc = r""] + #[doc = r" The experimental [`#[simd]`](https://docs.rs/fearless_simd_macros/latest/fearless_simd_macros/attr.simd.html)"] + #[doc = r" attribute from the separate `fearless_simd_macros` crate performs this wrapping"] + #[doc = r" automatically. Calling `vectorize()` directly is the dependency-free equivalent."] #[doc = r""] #[doc = r" # Example"] #[doc = r""] - #[doc = r" `double_u32s` is deliberately not marked `#[inline(always)]`. Instead, only its"] - #[doc = r" closure is inlined into the target-feature-enabled boundary created by `vectorize()`."] + #[doc = r" `double_u32s` does not need to be marked `#[inline(always)]`; only the closure passed"] + #[doc = r" to `vectorize()` does."] #[doc = r""] #[doc = r" ```"] #[doc = r" use fearless_simd::{dispatch, prelude::*, Level};"] diff --git a/fearless_simd/src/generated/wasm.rs b/fearless_simd/src/generated/wasm.rs index 5ce55edd..b557912d 100644 --- a/fearless_simd/src/generated/wasm.rs +++ b/fearless_simd/src/generated/wasm.rs @@ -194,6 +194,7 @@ impl Simd for WasmSimd128 { } #[inline(always)] fn vectorize R, R>(self, f: F) -> R { + #[inline] fn vectorize_inner R, R>(f: F) -> R { f() } diff --git a/fearless_simd/src/lib.rs b/fearless_simd/src/lib.rs index fd143035..3dd6f804 100644 --- a/fearless_simd/src/lib.rs +++ b/fearless_simd/src/lib.rs @@ -9,18 +9,26 @@ //! No matter what level of abstraction you're after, be it autovectorization and multiversioning, or portable SIMD, or safe access to raw //! intrinsics and nothing more, `fearless_simd` has you covered! //! -//! Zero dependencies, safe public APIs, and [very little](https://shnatsel.github.io/safe-simd-in-rust-even-on-the-inside/) `unsafe` under the hood. +//! The core crate has zero dependencies, safe public APIs, and +//! [very little](https://shnatsel.github.io/safe-simd-in-rust-even-on-the-inside/) +//! `unsafe` under the hood. //! //! # Automatic vectorization //! -//! Put the code to vectorize in an `#[inline(always)]` function generic over [`Simd`]. +//! The easiest way to define a SIMD-generic function is the experimental +//! [`#[simd]`](https://docs.rs/fearless_simd_macros/latest/fearless_simd_macros/attr.simd.html) +//! attribute from the separately versioned `fearless_simd_macros` crate. The companion macro +//! crate is optional: `fearless_simd` does not depend on it, so users of only the core API do not +//! pay for its procedural-macro dependencies. //! -//! This will generate several implementations for different SIMD levels and select the best one at runtime: +//! [`dispatch`] generates implementations for the available SIMD levels and selects the best one +//! at runtime: //! -//! ```rust +//! ```ignore //! use fearless_simd::{dispatch, Level, Simd}; +//! use fearless_simd_macros::simd; //! -//! #[inline(always)] +//! #[simd] //! fn double_u32s(_: S, values: &mut [u32]) { //! for value in values { //! *value = *value * 2; @@ -33,14 +41,38 @@ //! assert_eq!(values, [2, 4, 6, 8, 10]); //! ``` //! +//! The attribute uses the public [`Simd::vectorize`] API. You can call it directly to get the same +//! behavior without depending on the macro crate: +//! +//! ```rust +//! use fearless_simd::{dispatch, Level, Simd}; +//! +//! fn double_u32s(simd: S, values: &mut [u32]) { +//! simd.vectorize( +//! #[inline(always)] +//! || { +//! for value in values { +//! *value = *value * 2; +//! } +//! }, +//! ); +//! } +//! +//! let mut values = [1, 2, 3, 4, 5]; +//! let level = Level::new(); +//! dispatch!(level, simd => double_u32s(simd, &mut values)); +//! assert_eq!(values, [2, 4, 6, 8, 10]); +//! ``` +//! //! # Portable SIMD //! //! Use the vector types for explicit lane-wise operations while staying generic over the SIMD level: //! -//! ```rust +//! ```ignore //! use fearless_simd::{dispatch, prelude::*, Level}; +//! use fearless_simd_macros::simd; //! -//! #[inline(always)] +//! #[simd] //! fn double_u32s(simd: S, values: &mut [u32]) { //! let mut chunks = values.chunks_exact_mut(S::u32s::N); // the CPU's native SIMD width //! for chunk in &mut chunks { @@ -101,13 +133,17 @@ //! //! # Inlining //! -//! Fearless SIMD relies heavily on Rust's inlining support to create functions which have the given target features enabled. +//! Fearless SIMD relies on Rust's inlining support to place SIMD code in a context with the +//! appropriate target features enabled. //! //! As a rule of thumb: //! -//! - All SIMD functions need `#[inline(always)]`. +//! - Put `#[simd]` on SIMD-generic functions, or wrap their bodies in [`Simd::vectorize`]. //! - Use [`dispatch`] when calling SIMD code from non-SIMD code. -//! - Use [`vectorize()`][Simd::vectorize] when calling SIMD from SIMD if you don't want to force inlining. +//! - A closure passed directly to [`Simd::vectorize`] needs `#[inline(always)]`; `#[simd]` adds +//! this closure annotation for you. +//! - Helpers that are not themselves wrapped by `#[simd]` or [`Simd::vectorize`] still need to be +//! inlined into a SIMD-enabled caller. //! //! [The article describing the design](https://shnatsel.github.io/safe-simd-in-rust-even-on-the-inside/#the-abi-would-like-a-word) covers why this is the //! case. There's also Q&A on [Zulip](https://xi.zulipchat.com/#narrow/channel/514230-simd/topic/inlining/with/546913433). diff --git a/fearless_simd/src/macros.rs b/fearless_simd/src/macros.rs index 7671e123..b2eb0355 100644 --- a/fearless_simd/src/macros.rs +++ b/fearless_simd/src/macros.rs @@ -20,8 +20,10 @@ /// as seen in [the examples](#examples). /// For clarity, it will only be executed once per execution of `dispatch`. /// -/// To guarantee target-feature-specific code generation, any functions called within the operation should -/// be `#[inline(always)]`. +/// To guarantee target-feature-specific code generation, functions called within the operation +/// should either establish their own context with [`Simd::vectorize`], use the experimental +/// [`#[simd]`](https://docs.rs/fearless_simd_macros/latest/fearless_simd_macros/attr.simd.html) +/// attribute, or be `#[inline(always)]` so that they are incorporated into this operation. /// /// Note that as an implementation detail of this macro, the operation will be executed inside a closure. /// This is what enables the target features to be enabled for the code inside the operation. @@ -46,6 +48,7 @@ /// [`Level`]: crate::Level /// [`Level::new()`]: crate::Level::new /// [`Simd`]: crate::Simd +/// [`Simd::vectorize`]: crate::Simd::vectorize #[macro_export] macro_rules! dispatch { ($level:expr, $simd:pat => $op:expr) => {{ diff --git a/fearless_simd_gen/src/level.rs b/fearless_simd_gen/src/level.rs index 84a6ecc8..51231bda 100644 --- a/fearless_simd_gen/src/level.rs +++ b/fearless_simd_gen/src/level.rs @@ -160,12 +160,10 @@ pub(crate) trait Level { unsafe { #vectorize(f) } } } else { - // This SIMD level doesn't do runtime feature detection/enabling, so we could just call the passed closure as-is. - // - // But the inner function is required to be annotated `#[inline(always)]`, - // so we wrap it in a function that isn't `#[inline(always)]` - // to let the compiler make its own inlining decisions, as opposed to forcing it to inline everything. + // This SIMD level doesn't require a target-feature transition. Keep the same + // inline-friendly helper shape as the target-feature-enabled implementations. quote! { + #[inline] fn vectorize_inner R, R>(f: F) -> R { f() } diff --git a/fearless_simd_gen/src/mk_simd_trait.rs b/fearless_simd_gen/src/mk_simd_trait.rs index 5cd38705..54492bdc 100644 --- a/fearless_simd_gen/src/mk_simd_trait.rs +++ b/fearless_simd_gen/src/mk_simd_trait.rs @@ -145,21 +145,25 @@ pub(crate) fn mk_simd_trait() -> TokenStream { /// This SIMD token's feature level. fn level(self) -> Level; - /// Call function with SIMD instructions enabled, without forcing [inlining](https://matklad.github.io/2021/07/09/inline-in-rust.html). + /// Call a closure with the instructions for this SIMD level enabled. /// - /// `vectorize()` will set the correct `#[target_feature]` annotations for the SIMD level. - /// The provided function should be `#[inline(always)]`, otherwise it may not - /// be able to utilize the best SIMD instructions available. - /// `vectorize()` itself acts as the function boundary in machine code. + /// `vectorize()` establishes the correct `#[target_feature]` context. The provided + /// closure should be `#[inline(always)]` so that its body is incorporated into that + /// context and can use all instructions supported by this SIMD level. /// - /// This is useful when the SIMD implementation has already been selected and you want - /// to keep a SIMD-generic function outlined instead of forcing the entire function to - /// be inlined into its caller. + /// The small wrappers used by `vectorize()` are eligible for inlining when the caller + /// has a compatible target-feature context. A function containing the call does not + /// itself need an `#[inline(always)]` annotation; a target-feature boundary is retained + /// whenever one is required. + /// + /// The experimental [`#[simd]`](https://docs.rs/fearless_simd_macros/latest/fearless_simd_macros/attr.simd.html) + /// attribute from the separate `fearless_simd_macros` crate performs this wrapping + /// automatically. Calling `vectorize()` directly is the dependency-free equivalent. /// /// # Example /// - /// `double_u32s` is deliberately not marked `#[inline(always)]`. Instead, only its - /// closure is inlined into the target-feature-enabled boundary created by `vectorize()`. + /// `double_u32s` does not need to be marked `#[inline(always)]`; only the closure passed + /// to `vectorize()` does. /// /// ``` /// use fearless_simd::{dispatch, prelude::*, Level}; diff --git a/fearless_simd_macros/Cargo.toml b/fearless_simd_macros/Cargo.toml new file mode 100644 index 00000000..74d0389c --- /dev/null +++ b/fearless_simd_macros/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "fearless_simd_macros" +version = "0.1.0" +license.workspace = true +edition.workspace = true +repository.workspace = true +rust-version.workspace = true +authors = ["Raph Levien "] +keywords = ["simd", "proc-macro"] +categories = ["hardware-support", "development-tools::procedural-macro-helpers"] +description = "Procedural macros for fearless_simd" +readme = "README.md" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } + +[lints] +workspace = true diff --git a/fearless_simd_macros/LICENSE-APACHE b/fearless_simd_macros/LICENSE-APACHE new file mode 100644 index 00000000..d9a10c0d --- /dev/null +++ b/fearless_simd_macros/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/fearless_simd_macros/LICENSE-MIT b/fearless_simd_macros/LICENSE-MIT new file mode 100644 index 00000000..f3d84348 --- /dev/null +++ b/fearless_simd_macros/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2018 Raph Levien + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/fearless_simd_macros/README.md b/fearless_simd_macros/README.md new file mode 100644 index 00000000..f51e1ba2 --- /dev/null +++ b/fearless_simd_macros/README.md @@ -0,0 +1,116 @@ +# Fearless SIMD macros + +This crate provides the experimental `#[simd]` attribute for +[`fearless_simd`](https://crates.io/crates/fearless_simd). It is versioned +separately so that the macro can evolve without adding a procedural-macro +dependency to `fearless_simd` itself. + +The macro currently targets the inline-friendly `vectorize` implementation on +the main branch. Until the next `fearless_simd` release containing that +implementation and the first macro release are published, use both packages +from Git: + +```toml +[dependencies] +fearless_simd = { git = "https://github.com/linebender/fearless_simd" } +fearless_simd_macros = { git = "https://github.com/linebender/fearless_simd" } +``` + +`fearless_simd_macros` 0.1.0 must be published only after that core release; +this example should then use the released core version and +`fearless_simd_macros = "0.1"`. + +Then apply `#[simd]` to a function whose first ordinary parameter is its SIMD +token: + +```rust,ignore +use fearless_simd::prelude::*; +use fearless_simd_macros::simd; + +#[simd] +fn double_u32s(simd: S, values: &mut [u32]) { + let mut chunks = values.chunks_exact_mut(S::u32s::N); + for chunk in &mut chunks { + let value = S::u32s::from_slice(simd, chunk); + (value * 2).store_slice(chunk); + } + for value in chunks.into_remainder() { + *value *= 2; + } +} +``` + +Conceptually, the macro expands the body to: + +```rust,ignore +fn double_u32s(simd: S, values: &mut [u32]) { + simd.vectorize( + #[inline(always)] + || { + // Original body. + }, + ) +} +``` + +The attributed closure ensures that the original body is inlined into the +target-feature-enabled function provided by `Simd::vectorize`. The macro does +not add an `#[inline]` attribute to the annotated function. Any existing +`#[inline]`, documentation, lint, conditional-compilation, or other function +attributes remain on that function unchanged. +Code-placement attributes such as `#[cold]` therefore continue to describe the +outer wrapper; their effects are not transferred to the generated closure or +the target-feature helper that executes it. + +The closure and `vectorize` call are tail expressions, so the original body's +value and type checking are preserved. + +## Accepted functions + +The first typed parameter after an optional `self` receiver is treated as the +SIMD token. It must be a by-value identifier, such as `simd: S` or +`mut simd: S`. An unused token may be written as `_: S`; the macro gives it a +private hygienic binding. Destructured, `ref`, and `binding @ pattern` +parameters are not supported for the token. Neither `#[cfg]` nor `#[cfg_attr]` +may be placed on that parameter. + +The macro accepts synchronous free functions, inherent methods, trait +implementation methods, and default trait methods. Generic parameters, `where` +clauses, return types, `unsafe`, and non-variadic `extern` ABIs are preserved. +Attribute arguments are not supported: write `#[simd]`, not `#[simd(...)]`. + +`async`, `const`, variadic, bodyless, and specialization `default fn` +functions are rejected. The attributes `#[track_caller]`, `#[unsafe(naked)]`, +and `#[instruction_set]` are also rejected because moving the body into a +closure would invalidate their semantics or body requirements. +`#[target_feature]` and other attributes are preserved. + +A trait's `#[track_caller]` attribute is inherited by its implementations, but +is not present in the implementation method's token stream when this macro +runs. Applying `#[simd]` to an implementation of a trait method declared with +`#[track_caller]` is therefore unsupported even though the macro cannot +diagnose it. + +## Execution boundaries and captures + +Only work performed while the function body is executing is covered by +`vectorize`. Code inside a returned future, closure, or lazy iterator runs +later and is not covered. Named helper functions do not inherit the enabled +target features; make them inlineable or annotate their own SIMD-generic body. +Recursive calls enter `vectorize` again. + +The original body becomes a non-`move` `FnOnce` closure. Rust infers each +capture mode from how the body uses its parameters. As with any closure +conversion, the destruction order of captured values is not a stable +substitute for function-parameter destruction order. Avoid relying on the +relative drop order of by-value parameters with observable destructors in a +`#[simd]` function. + +This procedural macro intentionally has no dependency on `fearless_simd`. +The selected token's type must therefore make the `vectorize` trait method +available, normally through an `S: Simd` bound. + +## Minimum supported Rust version + +This version of `fearless_simd_macros` has been verified to compile with Rust +1.89 and later. Future versions may increase this requirement. diff --git a/fearless_simd_macros/src/lib.rs b/fearless_simd_macros/src/lib.rs new file mode 100644 index 00000000..cada1184 --- /dev/null +++ b/fearless_simd_macros/src/lib.rs @@ -0,0 +1,500 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +#![doc = include_str!("../README.md")] + +use core::mem; +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; +use quote::quote; +use syn::{AttrStyle, Attribute, FnArg, ItemFn, Pat, PatIdent, Result}; + +/// Run a SIMD-generic function body with the token's target features enabled. +/// +/// The first typed parameter after an optional `self` receiver is used as the +/// SIMD token. See the [crate-level documentation](crate) for the complete +/// expansion, supported function forms, and semantic caveats. +#[proc_macro_attribute] +pub fn simd(args: TokenStream, item: TokenStream) -> TokenStream { + expand(args.into(), item.into()) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +fn expand(args: TokenStream2, item: TokenStream2) -> Result { + if !args.is_empty() { + return Err(syn::Error::new_spanned( + args, + "`#[simd]` does not accept arguments", + )); + } + + // ItemFn's signature grammar also accepts body-bearing inherent and trait + // methods. Parsing the item rejects bodyless and specialization methods. + let mut function: ItemFn = syn::parse2(item)?; + + function.modifiers.require_empty()?; + reject_unsupported_signature(&function)?; + reject_unsupported_attributes(&function.attrs)?; + + let token = simd_token(&mut function)?; + let original_statements = mem::take(&mut function.block.stmts); + + // Inner function attributes are held in function.attrs by Syn. Leaving + // them there keeps them at the beginning of the outer function body, + // rather than changing their scope by moving them into this closure. + // Keep generated wrapper tokens on their normal macro-expansion spans. + // Giving the entire call the token parameter's source span makes Clippy's + // `semicolon_if_nothing_returned` lint fire on unit-returning functions. + let vectorize_call: syn::Expr = syn::parse_quote! { + #token.vectorize( + #[inline(always)] + || { #(#original_statements)* } + ) + }; + function + .block + .stmts + .push(syn::Stmt::Expr(vectorize_call, None)); + + Ok(quote!(#function)) +} + +fn reject_unsupported_signature(function: &ItemFn) -> Result<()> { + if let Some(asyncness) = &function.sig.asyncness { + return Err(syn::Error::new( + asyncness.span, + "`#[simd]` does not support async functions", + )); + } + if let Some(constness) = &function.sig.constness { + return Err(syn::Error::new( + constness.span, + "`#[simd]` does not support const functions", + )); + } + if let Some(variadic) = &function.sig.variadic { + return Err(syn::Error::new_spanned( + variadic, + "`#[simd]` does not support variadic functions", + )); + } + Ok(()) +} + +fn reject_unsupported_attributes(attrs: &[Attribute]) -> Result<()> { + for attr in attrs { + if !matches!(attr.style, AttrStyle::Outer) { + continue; + } + + let reason = if is_attribute(attr, "track_caller") { + Some("`#[simd]` cannot preserve `#[track_caller]` through its closure") + } else if is_attribute(attr, "naked") { + Some("`#[simd]` cannot be used on a naked function") + } else if is_attribute(attr, "instruction_set") { + Some("`#[simd]` cannot be combined with `#[instruction_set]`") + } else { + None + }; + + if let Some(reason) = reason { + return Err(syn::Error::new_spanned(attr, reason)); + } + } + Ok(()) +} + +fn is_attribute(attr: &Attribute, name: &str) -> bool { + if attr.path().is_ident(name) { + return true; + } + + // Attributes with safety obligations use `#[unsafe(attribute)]` syntax. + // Naked functions require this form on supported Rust releases. + attr.path().is_ident("unsafe") + && attr + .parse_args::() + .is_ok_and(|path| path.is_ident(name)) +} + +fn simd_token(function: &mut ItemFn) -> Result { + let Some(argument) = function + .sig + .inputs + .iter_mut() + .find_map(|argument| match argument { + FnArg::Receiver(_) => None, + FnArg::Typed(argument) => Some(argument), + }) + else { + return Err(syn::Error::new_spanned( + &function.sig.inputs, + "`#[simd]` requires a SIMD token parameter after any receiver", + )); + }; + + reject_conditional_attributes(&argument.attrs)?; + + match &mut *argument.pat { + Pat::Ident(pattern) => { + reject_conditional_attributes(&pattern.attrs)?; + if let Some(by_ref) = &pattern.by_ref { + return Err(syn::Error::new( + by_ref.span, + "the SIMD token parameter must be bound by value, not `ref`", + )); + } + if let Some((at, _)) = &pattern.subpat { + return Err(syn::Error::new( + at.span, + "the SIMD token parameter cannot use an `@` subpattern", + )); + } + Ok(pattern.ident.clone()) + } + Pat::Wild(pattern) => { + reject_conditional_attributes(&pattern.attrs)?; + let token = Ident::new("__fearless_simd_token", Span::mixed_site()); + let attrs = mem::take(&mut pattern.attrs); + *argument.pat = Pat::Ident(PatIdent { + attrs, + by_ref: None, + mutability: None, + ident: token.clone(), + subpat: None, + }); + Ok(token) + } + pattern => Err(syn::Error::new_spanned( + pattern, + "the SIMD token parameter must be an identifier or `_`", + )), + } +} + +fn reject_conditional_attributes(attrs: &[Attribute]) -> Result<()> { + if let Some(attr) = attrs + .iter() + .find(|attr| attr.path().is_ident("cfg") || attr.path().is_ident("cfg_attr")) + { + return Err(syn::Error::new_spanned( + attr, + "the SIMD token parameter cannot be conditional", + )); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::expand; + use quote::{ToTokens, quote}; + use syn::{AttrStyle, Expr, ItemFn, Stmt}; + + fn expand_ok(item: proc_macro2::TokenStream) -> proc_macro2::TokenStream { + expand(proc_macro2::TokenStream::new(), item).expect("macro expansion should succeed") + } + + fn expand_err(item: proc_macro2::TokenStream) -> String { + expand(proc_macro2::TokenStream::new(), item) + .expect_err("macro expansion should fail") + .to_string() + } + + #[test] + fn expands_body_as_an_attributed_tail_closure() { + let expanded = expand_ok(quote! { + fn add(simd: S, lhs: u32, rhs: u32) -> u32 { + let sum = lhs + rhs; + sum + } + }); + let parsed: ItemFn = syn::parse2(expanded).expect("expanded function parses"); + let Some(Stmt::Expr(Expr::MethodCall(call), None)) = parsed.block.stmts.last() else { + panic!("function tail should be a method call"); + }; + let Some(Expr::Closure(closure)) = call.args.first() else { + panic!("vectorize argument should be a closure"); + }; + let Expr::Block(body) = &*closure.body else { + panic!("closure body should be a block"); + }; + + assert_eq!(call.method, "vectorize"); + assert_eq!(call.args.len(), 1); + assert!(closure.capture.is_none()); + assert_eq!(closure.attrs.len(), 1); + assert!(closure.attrs[0].path().is_ident("inline")); + let inline_kind: proc_macro2::Ident = closure.attrs[0] + .parse_args() + .expect("inline attribute has one identifier argument"); + assert_eq!(inline_kind, "always"); + assert_eq!(body.block.stmts.len(), 2); + assert!( + parsed + .attrs + .iter() + .all(|attr| !attr.path().is_ident("inline")) + ); + } + + #[test] + fn unit_returns_remain_tail_expressions() { + for item in [ + quote! { + fn implicit_unit(simd: S) { let _ = simd.level(); } + }, + quote! { + fn explicit_unit(simd: S) -> () { let _ = simd.level(); } + }, + ] { + let expanded = expand_ok(item); + let parsed: ItemFn = syn::parse2(expanded).expect("expanded function parses"); + let Some(Stmt::Expr(Expr::MethodCall(call), None)) = parsed.block.stmts.last() else { + panic!("unit function tail should be a method call"); + }; + let Some(Expr::Closure(closure)) = call.args.first() else { + panic!("vectorize argument should be a closure"); + }; + + assert!(matches!(closure.output, syn::ReturnType::Default)); + } + } + + #[test] + fn non_unit_return_remains_a_tail_expression() { + let expanded = expand_ok(quote! { + fn non_unit(simd: S) -> u32 { 42 } + }); + let parsed: ItemFn = syn::parse2(expanded).expect("expanded function parses"); + + assert!(matches!( + parsed.block.stmts.last(), + Some(Stmt::Expr(Expr::MethodCall(_), None)) + )); + } + + #[test] + fn preserves_signature_attributes_and_inner_attributes() { + let expanded = expand_ok(quote! { + #[doc = "docs"] + #[inline(never)] + #[target_feature(enable = "sse2")] + unsafe extern "C" fn operation<'a, S, T>(simd: S, value: &'a T) -> &'a T + where + S: Simd, + { + #![allow(unused_unsafe)] + unsafe { value } + } + }); + let parsed: ItemFn = syn::parse2(expanded.clone()).expect("expanded function parses"); + + assert!(matches!(parsed.sig.safety, syn::Safety::Unsafe(_))); + assert!(parsed.sig.abi.is_some()); + assert!(parsed.sig.generics.where_clause.is_some()); + assert_eq!( + parsed + .attrs + .iter() + .filter(|attr| matches!(attr.style, AttrStyle::Inner(_))) + .count(), + 1 + ); + + let text = expanded.to_string(); + let inner_attr = text + .find("# ! [allow") + .expect("inner attribute is retained"); + let call = text + .find("simd . vectorize") + .expect("vectorize call exists"); + assert!(inner_attr < call); + assert_eq!(text.matches("inline (never)").count(), 1); + assert_eq!(text.matches("inline (always)").count(), 1); + assert!(text.contains("target_feature")); + } + + #[test] + fn selects_first_typed_parameter_after_receiver() { + let expanded = expand_ok(quote! { + fn method(&self, mut backend: S, value: u32) -> u32 { + backend.level(); + value + } + }); + let text = expanded.to_string(); + + assert!(text.contains("mut backend : S")); + assert!(text.contains("backend . vectorize")); + } + + #[test] + fn gives_a_wildcard_token_a_private_binding() { + let expanded = expand_ok(quote! { + fn operation(_: S, value: u32) -> u32 { value } + }); + let text = expanded.to_string(); + + assert_eq!(text.matches("__fearless_simd_token").count(), 2); + assert!(text.contains("__fearless_simd_token : S")); + assert!(text.contains("__fearless_simd_token . vectorize")); + } + + #[test] + fn wildcard_binding_does_not_rename_a_user_binding_with_the_same_spelling() { + let expanded = expand_ok(quote! { + fn operation(_: S, __fearless_simd_token: u32) -> u32 { + __fearless_simd_token + } + }); + let parsed: ItemFn = syn::parse2(expanded).expect("expanded function parses"); + + assert_eq!(parsed.sig.inputs.len(), 2); + assert_eq!( + parsed + .to_token_stream() + .to_string() + .matches("__fearless_simd_token") + .count(), + 4 + ); + } + + #[test] + fn accepts_default_trait_method_syntax() { + let expanded = expand_ok(quote! { + fn operation(&self, simd: S) -> u32 { 42 } + }); + assert!(expanded.to_string().contains("simd . vectorize")); + } + + #[test] + fn rejects_attribute_arguments() { + let error = expand( + quote!(token = simd), + quote!( + fn f(simd: S) {} + ), + ) + .expect_err("arguments should be rejected") + .to_string(); + assert_eq!(error, "`#[simd]` does not accept arguments"); + } + + #[test] + fn rejects_unsupported_signatures() { + assert!( + expand_err(quote!( + async fn f(simd: S) {} + )) + .contains("async functions") + ); + assert!( + expand_err(quote!( + const fn f(simd: S) {} + )) + .contains("const functions") + ); + assert!( + expand_err(quote!( + unsafe extern "C" fn f(simd: S, ...) {} + )) + .contains("variadic functions") + ); + assert!(expand(quote!(), quote!(default fn f(simd: S) {})).is_err()); + } + + #[test] + fn rejects_unsupported_function_attributes() { + assert!( + expand_err(quote!( + #[track_caller] + fn f(simd: S) {} + )) + .contains("cannot preserve") + ); + assert!( + expand_err(quote!( + #[naked] + fn f(simd: S) {} + )) + .contains("naked") + ); + assert!( + expand_err(quote!( + #[unsafe(naked)] + fn f(simd: S) {} + )) + .contains("naked") + ); + assert!( + expand_err(quote!( + #[instruction_set(arm::a32)] + fn f(simd: S) {} + )) + .contains("instruction_set") + ); + } + + #[test] + fn rejects_missing_or_unsupported_token_patterns() { + assert!( + expand_err(quote!( + fn f() {} + )) + .contains("requires a SIMD token") + ); + assert!( + expand_err(quote!( + fn f(ref simd: S) {} + )) + .contains("bound by value") + ); + assert!( + expand_err(quote!( + fn f(simd @ _: S) {} + )) + .contains("subpattern") + ); + assert!( + expand_err(quote!( + fn f((simd, _): (S, u32)) {} + )) + .contains("identifier or `_`") + ); + } + + #[test] + fn rejects_conditional_token_parameters() { + assert!( + expand_err(quote!( + fn f(#[cfg(any())] simd: S) {} + )) + .contains("cannot be conditional") + ); + assert!( + expand_err(quote!( + fn f(#[cfg_attr(any(), allow(unused))] simd: S) {} + )) + .contains("cannot be conditional") + ); + } + + #[test] + fn rejects_non_functions_and_bodyless_functions() { + assert!( + expand_err(quote!( + struct NotAFunction; + )) + .contains("expected") + ); + assert!( + expand_err(quote!( + fn bodyless(simd: S); + )) + .contains("expected") + ); + } +} diff --git a/fearless_simd_tests/Cargo.toml b/fearless_simd_tests/Cargo.toml index 0d00bf94..c496ef65 100644 --- a/fearless_simd_tests/Cargo.toml +++ b/fearless_simd_tests/Cargo.toml @@ -9,20 +9,32 @@ description = "Safer and easier SIMD" readme = "README.md" rust-version.workspace = true publish = false -# Tests are collected through tests/mod.rs; files beside it are modules, not separate integration crates. +# Functional tests are collected through tests/mod.rs; files beside it are modules rather than +# separate integration crates. The simd_ui harness is declared explicitly below. autotests = false [[test]] name = "tests" path = "tests/mod.rs" +[[test]] +name = "simd_ui" +path = "tests/simd_ui.rs" + [lints] workspace = true [dependencies] +fearless_simd = { workspace = true, features = ["libm"] } +fearless_simd_macros = { workspace = true } + +[dev-dependencies] fastrand = "2.5.0" fearless_simd = { workspace = true, features = [ "std", "force_support_fallback", ] } fearless_simd_dev_macros = { workspace = true } + +[target.'cfg(all(target_arch = "x86_64", target_os = "linux"))'.dev-dependencies] +trybuild = "1.0.120" diff --git a/fearless_simd_tests/src/lib.rs b/fearless_simd_tests/src/lib.rs new file mode 100644 index 00000000..f4e8e5da --- /dev/null +++ b/fearless_simd_tests/src/lib.rs @@ -0,0 +1,20 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Compile-only fixtures for testing `fearless_simd` without the standard library. + +#![no_std] + +use fearless_simd::Simd; +use fearless_simd_macros::simd; + +/// Doubles every value using a function annotated with [`simd`]. +/// +/// This function intentionally lives in the library target so CI can compile it for a target +/// where the standard library is unavailable. +#[simd] +pub fn double_without_std(_simd: S, values: &mut [u32]) { + for value in values { + *value *= 2; + } +} diff --git a/fearless_simd_tests/tests/mod.rs b/fearless_simd_tests/tests/mod.rs index 2067c8ae..ddd093b9 100644 --- a/fearless_simd_tests/tests/mod.rs +++ b/fearless_simd_tests/tests/mod.rs @@ -15,6 +15,7 @@ use fearless_simd_dev_macros::simd_test; mod generics; mod harness; +mod simd_macro; #[cfg(not(miri))] // too slow mod soundness; mod token_soundness; diff --git a/fearless_simd_tests/tests/simd_macro.rs b/fearless_simd_tests/tests/simd_macro.rs new file mode 100644 index 00000000..f3a0ebc1 --- /dev/null +++ b/fearless_simd_tests/tests/simd_macro.rs @@ -0,0 +1,98 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd::prelude::*; +use fearless_simd::{Simd, u32x8}; +use fearless_simd_dev_macros::simd_test; +use fearless_simd_macros::simd; + +#[simd] +fn double_values(_simd: S, values: &mut [u32]) { + for value in values { + *value *= 2; + } +} + +#[simd] +fn add_vectors(simd: S, left: [u32; 8], right: [u32; 8]) -> [u32; 8] { + let left = u32x8::from_slice(simd, &left); + let right = u32x8::from_slice(simd, &right); + (left + right).into() +} + +#[simd] +fn increment_first(_simd: S, values: &mut [u32]) -> Result<(), &'static str> { + let first = values.first_mut().ok_or("slice must not be empty")?; + *first += 1; + Ok(()) +} + +#[simd] +fn wildcard_token(_: S, value: u32) -> u32 { + value + 1 +} + +struct MethodFixture(u32); + +impl MethodFixture { + #[simd] + fn add(&self, _simd: S, value: u32) -> u32 { + self.0 + value + } +} + +trait DefaultMethodFixture { + #[simd] + fn subtract(&self, _simd: S, value: u32) -> u32 { + value - 1 + } +} + +impl DefaultMethodFixture for MethodFixture {} + +#[simd_test] +fn simd_attribute_runs_on_every_backend(simd: S) { + let mut values = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + double_values(simd, &mut values); + assert_eq!( + values, + [2, 4, 6, 8, 10, 12, 14, 16, 18], + "the autovectorized body should execute inside vectorize" + ); + + let sum = add_vectors(simd, [1, 2, 3, 4, 5, 6, 7, 8], [8, 7, 6, 5, 4, 3, 2, 1]); + assert_eq!( + sum, [9; 8], + "explicit portable vector operations should return their result" + ); + + assert_eq!( + increment_first(simd, &mut values), + Ok(()), + "the success path should propagate through vectorize" + ); + assert_eq!(values[0], 3, "the success path should mutate its input"); + assert_eq!( + increment_first(simd, &mut []), + Err("slice must not be empty"), + "the question-mark operator should return from the wrapped body" + ); + + assert_eq!( + wildcard_token(simd, 41), + 42, + "a wildcard SIMD parameter should receive a hidden binding" + ); + + let fixture = MethodFixture(10); + assert_eq!( + fixture.add(simd, 5), + 15, + "the first typed parameter after an inherent receiver should be selected" + ); + assert_eq!( + fixture.subtract(simd, 5), + 4, + "the macro should support default trait methods" + ); +} diff --git a/fearless_simd_tests/tests/simd_ui.rs b/fearless_simd_tests/tests/simd_ui.rs new file mode 100644 index 00000000..117ea5a4 --- /dev/null +++ b/fearless_simd_tests/tests/simd_ui.rs @@ -0,0 +1,13 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +#![expect(missing_docs, reason = "this integration test is not public API")] + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +#[test] +#[ignore = "run explicitly in the native Linux CI job"] +fn simd_ui() { + let tests = trybuild::TestCases::new(); + tests.pass("tests/ui/simd/pass/*.rs"); + tests.compile_fail("tests/ui/simd/fail/*.rs"); +} diff --git a/fearless_simd_tests/tests/ui/simd/fail/arguments.rs b/fearless_simd_tests/tests/ui/simd/fail/arguments.rs new file mode 100644 index 00000000..ddd50d91 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/arguments.rs @@ -0,0 +1,11 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +#[simd(token = simd)] +fn arguments_are_not_supported(simd: S) { + let _ = simd; +} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/arguments.stderr b/fearless_simd_tests/tests/ui/simd/fail/arguments.stderr new file mode 100644 index 00000000..d137a0ab --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/arguments.stderr @@ -0,0 +1,5 @@ +error: `#[simd]` does not accept arguments + --> tests/ui/simd/fail/arguments.rs:6:8 + | +6 | #[simd(token = simd)] + | ^^^^^^^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/async.rs b/fearless_simd_tests/tests/ui/simd/fail/async.rs new file mode 100644 index 00000000..cbde7bf0 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/async.rs @@ -0,0 +1,11 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +#[simd] +async fn async_is_not_supported(simd: S) { + let _ = simd; +} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/async.stderr b/fearless_simd_tests/tests/ui/simd/fail/async.stderr new file mode 100644 index 00000000..17cdda2b --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/async.stderr @@ -0,0 +1,5 @@ +error: `#[simd]` does not support async functions + --> tests/ui/simd/fail/async.rs:7:1 + | +7 | async fn async_is_not_supported(simd: S) { + | ^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/at_token.rs b/fearless_simd_tests/tests/ui/simd/fail/at_token.rs new file mode 100644 index 00000000..fe49fa1b --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/at_token.rs @@ -0,0 +1,11 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +#[simd] +fn at_token_is_not_supported(simd @ _: S) { + let _ = simd; +} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/at_token.stderr b/fearless_simd_tests/tests/ui/simd/fail/at_token.stderr new file mode 100644 index 00000000..a4c19dd7 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/at_token.stderr @@ -0,0 +1,5 @@ +error: the SIMD token parameter cannot use an `@` subpattern + --> tests/ui/simd/fail/at_token.rs:7:59 + | +7 | fn at_token_is_not_supported(simd @ _: S) { + | ^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/bodyless.rs b/fearless_simd_tests/tests/ui/simd/fail/bodyless.rs new file mode 100644 index 00000000..2ae8b7f4 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/bodyless.rs @@ -0,0 +1,11 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +trait Operation { + #[simd] + fn bodyless(simd: S); +} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/bodyless.stderr b/fearless_simd_tests/tests/ui/simd/fail/bodyless.stderr new file mode 100644 index 00000000..3852d821 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/bodyless.stderr @@ -0,0 +1,5 @@ +error: expected curly braces + --> tests/ui/simd/fail/bodyless.rs:8:49 + | +8 | fn bodyless(simd: S); + | ^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/const.rs b/fearless_simd_tests/tests/ui/simd/fail/const.rs new file mode 100644 index 00000000..228233c4 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/const.rs @@ -0,0 +1,11 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +#[simd] +const fn const_is_not_supported(simd: S) { + let _ = simd; +} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/const.stderr b/fearless_simd_tests/tests/ui/simd/fail/const.stderr new file mode 100644 index 00000000..b4bd65fe --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/const.stderr @@ -0,0 +1,5 @@ +error: `#[simd]` does not support const functions + --> tests/ui/simd/fail/const.rs:7:1 + | +7 | const fn const_is_not_supported(simd: S) { + | ^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/default_modifier.rs b/fearless_simd_tests/tests/ui/simd/fail/default_modifier.rs new file mode 100644 index 00000000..a21b5f90 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/default_modifier.rs @@ -0,0 +1,21 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +struct Fixture; + +trait Operation { + fn operation(simd: S) { + let _ = simd; + } +} + +impl Operation for Fixture { + #[simd] + default fn operation(simd: S) { + let _ = simd; + } +} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/default_modifier.stderr b/fearless_simd_tests/tests/ui/simd/fail/default_modifier.stderr new file mode 100644 index 00000000..978a49f2 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/default_modifier.stderr @@ -0,0 +1,5 @@ +error: expected `fn` + --> tests/ui/simd/fail/default_modifier.rs:16:5 + | +16 | default fn operation(simd: S) { + | ^^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/destructured_token.rs b/fearless_simd_tests/tests/ui/simd/fail/destructured_token.rs new file mode 100644 index 00000000..480f1e90 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/destructured_token.rs @@ -0,0 +1,11 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +#[simd] +fn destructured_token_is_not_supported((simd,): (S,)) { + let _ = simd; +} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/destructured_token.stderr b/fearless_simd_tests/tests/ui/simd/fail/destructured_token.stderr new file mode 100644 index 00000000..e82ccef1 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/destructured_token.stderr @@ -0,0 +1,5 @@ +error: the SIMD token parameter must be an identifier or `_` + --> tests/ui/simd/fail/destructured_token.rs:7:64 + | +7 | fn destructured_token_is_not_supported((simd,): (S,)) { + | ^^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/instruction_set.rs b/fearless_simd_tests/tests/ui/simd/fail/instruction_set.rs new file mode 100644 index 00000000..7a0e2653 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/instruction_set.rs @@ -0,0 +1,12 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +#[simd] +#[instruction_set(arm::a32)] +fn instruction_set_is_not_supported(simd: S) { + let _ = simd; +} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/instruction_set.stderr b/fearless_simd_tests/tests/ui/simd/fail/instruction_set.stderr new file mode 100644 index 00000000..c09066da --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/instruction_set.stderr @@ -0,0 +1,5 @@ +error: `#[simd]` cannot be combined with `#[instruction_set]` + --> tests/ui/simd/fail/instruction_set.rs:7:1 + | +7 | #[instruction_set(arm::a32)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/naked.rs b/fearless_simd_tests/tests/ui/simd/fail/naked.rs new file mode 100644 index 00000000..616a3967 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/naked.rs @@ -0,0 +1,12 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +#[simd] +#[naked] +fn naked_is_not_supported(simd: S) { + let _ = simd; +} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/naked.stderr b/fearless_simd_tests/tests/ui/simd/fail/naked.stderr new file mode 100644 index 00000000..ff9ad10b --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/naked.stderr @@ -0,0 +1,5 @@ +error: `#[simd]` cannot be used on a naked function + --> tests/ui/simd/fail/naked.rs:7:1 + | +7 | #[naked] + | ^^^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/no_token.rs b/fearless_simd_tests/tests/ui/simd/fail/no_token.rs new file mode 100644 index 00000000..3a355ded --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/no_token.rs @@ -0,0 +1,9 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +#[simd] +fn token_is_required() {} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/no_token.stderr b/fearless_simd_tests/tests/ui/simd/fail/no_token.stderr new file mode 100644 index 00000000..dcc3d7dc --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/no_token.stderr @@ -0,0 +1,7 @@ +error: `#[simd]` requires a SIMD token parameter after any receiver + --> tests/ui/simd/fail/no_token.rs:6:1 + | +6 | #[simd] + | ^^^^^^^ + | + = note: this error originates in the attribute macro `simd` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/fearless_simd_tests/tests/ui/simd/fail/non_function.rs b/fearless_simd_tests/tests/ui/simd/fail/non_function.rs new file mode 100644 index 00000000..55e92ec9 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/non_function.rs @@ -0,0 +1,9 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +#[simd] +struct NotAFunction; + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/non_function.stderr b/fearless_simd_tests/tests/ui/simd/fail/non_function.stderr new file mode 100644 index 00000000..54803088 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/non_function.stderr @@ -0,0 +1,5 @@ +error: expected `fn` + --> tests/ui/simd/fail/non_function.rs:7:1 + | +7 | struct NotAFunction; + | ^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.rs b/fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.rs new file mode 100644 index 00000000..135db6b7 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.rs @@ -0,0 +1,11 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +#[simd] +fn conditional_token_is_not_supported(#[cfg(any())] simd: S) { + let _ = simd; +} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.stderr b/fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.stderr new file mode 100644 index 00000000..b6049c92 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.stderr @@ -0,0 +1,5 @@ +error: the SIMD token parameter cannot be conditional + --> tests/ui/simd/fail/parameter_cfg.rs:7:63 + | +7 | fn conditional_token_is_not_supported(#[cfg(any())] simd: S) { + | ^^^^^^^^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/ref_token.rs b/fearless_simd_tests/tests/ui/simd/fail/ref_token.rs new file mode 100644 index 00000000..7dfb8d7b --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/ref_token.rs @@ -0,0 +1,11 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +#[simd] +fn ref_token_is_not_supported(ref simd: S) { + let _ = simd; +} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/ref_token.stderr b/fearless_simd_tests/tests/ui/simd/fail/ref_token.stderr new file mode 100644 index 00000000..8eab084d --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/ref_token.stderr @@ -0,0 +1,5 @@ +error: the SIMD token parameter must be bound by value, not `ref` + --> tests/ui/simd/fail/ref_token.rs:7:55 + | +7 | fn ref_token_is_not_supported(ref simd: S) { + | ^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/track_caller.rs b/fearless_simd_tests/tests/ui/simd/fail/track_caller.rs new file mode 100644 index 00000000..7be360d3 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/track_caller.rs @@ -0,0 +1,12 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +#[simd] +#[track_caller] +fn track_caller_is_not_supported(simd: S) { + let _ = simd; +} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/track_caller.stderr b/fearless_simd_tests/tests/ui/simd/fail/track_caller.stderr new file mode 100644 index 00000000..e6411950 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/track_caller.stderr @@ -0,0 +1,5 @@ +error: `#[simd]` cannot preserve `#[track_caller]` through its closure + --> tests/ui/simd/fail/track_caller.rs:7:1 + | +7 | #[track_caller] + | ^^^^^^^^^^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/variadic.rs b/fearless_simd_tests/tests/ui/simd/fail/variadic.rs new file mode 100644 index 00000000..e44394cb --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/variadic.rs @@ -0,0 +1,11 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd_macros::simd; + +#[simd] +unsafe extern "C" fn variadic_is_not_supported(simd: S, _: ...) { + let _ = simd; +} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/variadic.stderr b/fearless_simd_tests/tests/ui/simd/fail/variadic.stderr new file mode 100644 index 00000000..a6e4d730 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/variadic.stderr @@ -0,0 +1,5 @@ +error: `#[simd]` does not support variadic functions + --> tests/ui/simd/fail/variadic.rs:7:81 + | +7 | unsafe extern "C" fn variadic_is_not_supported(simd: S, _: ...) { + | ^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/pass/functions.rs b/fearless_simd_tests/tests/ui/simd/pass/functions.rs new file mode 100644 index 00000000..ac1caf37 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/pass/functions.rs @@ -0,0 +1,74 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd::{Fallback, Simd}; +use fearless_simd_macros::simd; + +#[simd] +#[inline(never)] +#[must_use] +fn with_generics<'a, S, T>(simd: S, value: &'a mut T) -> Result<&'a mut T, ()> +where + S: Simd, +{ + let _ = simd.level(); + Ok(value) +} + +#[simd] +#[inline] +fn with_inline(_simd: S, value: u32) -> u32 { + value +} + +#[simd] +#[inline(always)] +fn with_inline_always(_simd: S, value: u32) -> u32 { + value +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +#[simd] +#[target_feature(enable = "sse2")] +unsafe fn with_target_feature(_simd: S, value: u32) -> u32 { + value +} + +#[simd] +fn mutable_token(mut simd: S, value: u32) -> u32 { + let _ = &mut simd; + value +} + +#[simd] +fn wildcard_token(_: S, value: u32) -> u32 { + value +} + +#[simd] +fn wildcard_binding_is_hygienic(_: S, __fearless_simd_token: u32) -> u32 { + __fearless_simd_token +} + +#[simd] +unsafe extern "Rust" fn unsafe_with_abi(simd: S, ptr: *const u32) -> u32 { + let _ = simd.level(); + // SAFETY: The caller promises that `ptr` is valid to read. + unsafe { ptr.read() } +} + +fn main() { + let fallback = Fallback::new(); + let mut value = 1; + assert_eq!(with_generics(fallback, &mut value), Ok(&mut 1)); + assert_eq!(with_inline(fallback, 2), 2); + assert_eq!(with_inline_always(fallback, 3), 3); + assert_eq!(mutable_token(fallback, 2), 2); + assert_eq!(wildcard_token(fallback, 3), 3); + assert_eq!(wildcard_binding_is_hygienic(fallback, 4), 4); + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + // SAFETY: SSE2 is part of the x86-64 baseline used by this UI test. + assert_eq!(unsafe { with_target_feature(fallback, 5) }, 5); + // SAFETY: The pointer is derived from a live local value. + assert_eq!(unsafe { unsafe_with_abi(fallback, &value) }, 1); +} diff --git a/fearless_simd_tests/tests/ui/simd/pass/methods.rs b/fearless_simd_tests/tests/ui/simd/pass/methods.rs new file mode 100644 index 00000000..8975a08b --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/pass/methods.rs @@ -0,0 +1,42 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fearless_simd::{Fallback, Simd}; +use fearless_simd_macros::simd; + +struct Fixture; + +impl Fixture { + #[simd] + fn inherent(&self, _simd: S, value: u32) -> u32 { + value + 1 + } +} + +trait Operation { + #[simd] + fn default_method(&self, _simd: S, value: u32) -> u32 { + value + 2 + } +} + +impl Operation for Fixture {} + +trait ImplementedOperation { + fn implemented(&self, simd: S, value: u32) -> u32; +} + +impl ImplementedOperation for Fixture { + #[simd] + fn implemented(&self, _simd: S, value: u32) -> u32 { + value + 3 + } +} + +fn main() { + let fixture = Fixture; + let fallback = Fallback::new(); + assert_eq!(fixture.inherent(fallback, 1), 2); + assert_eq!(fixture.default_method(fallback, 1), 3); + assert_eq!(fixture.implemented(fallback, 1), 4); +} From 0e83ed91741d061a10dd0aa7c2bcae38b8c78511 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Mon, 24 Aug 2026 18:24:09 +0100 Subject: [PATCH 07/20] placate clippy --- fearless_simd/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fearless_simd/src/lib.rs b/fearless_simd/src/lib.rs index 3dd6f804..d70ae555 100644 --- a/fearless_simd/src/lib.rs +++ b/fearless_simd/src/lib.rs @@ -252,8 +252,9 @@ extern crate std; #[cfg(all(not(feature = "libm"), not(feature = "std")))] compile_error!("fearless_simd requires either the `std` or `libm` feature"); -// Suppress the unused_crate_dependencies lint when both std and libm are specified. -#[cfg(all(feature = "std", feature = "libm"))] +// Some native SIMD backends do not need libm unless fallback support is also enabled. +// Keep the optional dependency marked as used for every supported target configuration. +#[cfg(feature = "libm")] use libm as _; mod generated; From e953125cb9dc6ecb9bdd088831b0d02d21e602d2 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 3 Sep 2026 12:35:24 +0100 Subject: [PATCH 08/20] Credit myself as the author of the _macros crate --- fearless_simd_macros/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fearless_simd_macros/Cargo.toml b/fearless_simd_macros/Cargo.toml index 74d0389c..d5e82349 100644 --- a/fearless_simd_macros/Cargo.toml +++ b/fearless_simd_macros/Cargo.toml @@ -5,7 +5,7 @@ license.workspace = true edition.workspace = true repository.workspace = true rust-version.workspace = true -authors = ["Raph Levien "] +authors = ["Sergey \"Shnatsel\" Davidoff "] keywords = ["simd", "proc-macro"] categories = ["hardware-support", "development-tools::procedural-macro-helpers"] description = "Procedural macros for fearless_simd" From 9ad80a4051d492431c3b4c080175e85a19dae842 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 4 Sep 2026 09:14:11 +0100 Subject: [PATCH 09/20] Revert "Make vectorize() #[inline(always)] instead of #[inline] so that it injects exactly one function boundary" This reverts commit ff8ee04b99f1974d33ab02077f877f273ea95762. --- fearless_simd/src/generated/avx2.rs | 2 +- fearless_simd/src/generated/avx512.rs | 2 +- fearless_simd/src/generated/fallback.rs | 2 +- fearless_simd/src/generated/neon.rs | 2 +- fearless_simd/src/generated/sse2.rs | 2 +- fearless_simd/src/generated/sse4_2.rs | 2 +- fearless_simd/src/generated/wasm.rs | 2 +- fearless_simd_gen/src/level.rs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/fearless_simd/src/generated/avx2.rs b/fearless_simd/src/generated/avx2.rs index 1f01ab8e..3cb8bd9d 100644 --- a/fearless_simd/src/generated/avx2.rs +++ b/fearless_simd/src/generated/avx2.rs @@ -105,7 +105,7 @@ impl Simd for Avx2 { fn level(self) -> Level { Level::Avx2(self) } - #[inline(always)] + #[inline] fn vectorize R, R>(self, f: F) -> R { #[inline] #[target_feature( diff --git a/fearless_simd/src/generated/avx512.rs b/fearless_simd/src/generated/avx512.rs index 05daf872..039732a5 100644 --- a/fearless_simd/src/generated/avx512.rs +++ b/fearless_simd/src/generated/avx512.rs @@ -379,7 +379,7 @@ impl Simd for Avx512 { fn level(self) -> Level { Level::Avx512(self) } - #[inline(always)] + #[inline] fn vectorize R, R>(self, f: F) -> R { #[inline] #[target_feature( diff --git a/fearless_simd/src/generated/fallback.rs b/fearless_simd/src/generated/fallback.rs index 04a612fa..3dffbe6f 100644 --- a/fearless_simd/src/generated/fallback.rs +++ b/fearless_simd/src/generated/fallback.rs @@ -190,7 +190,7 @@ impl Simd for Fallback { #[cfg(not(feature = "force_support_fallback"))] Level::baseline() } - #[inline(always)] + #[inline] fn vectorize R, R>(self, f: F) -> R { #[inline] fn vectorize_inner R, R>(f: F) -> R { diff --git a/fearless_simd/src/generated/neon.rs b/fearless_simd/src/generated/neon.rs index ec8ae7ed..7c989c5d 100644 --- a/fearless_simd/src/generated/neon.rs +++ b/fearless_simd/src/generated/neon.rs @@ -100,7 +100,7 @@ impl Simd for Neon { fn level(self) -> Level { Level::Neon(self) } - #[inline(always)] + #[inline] fn vectorize R, R>(self, f: F) -> R { #[inline] #[target_feature(enable = "neon")] diff --git a/fearless_simd/src/generated/sse2.rs b/fearless_simd/src/generated/sse2.rs index 6551417e..1a600662 100644 --- a/fearless_simd/src/generated/sse2.rs +++ b/fearless_simd/src/generated/sse2.rs @@ -203,7 +203,7 @@ impl Simd for Sse2 { fn level(self) -> Level { Level::Sse2(self) } - #[inline(always)] + #[inline] fn vectorize R, R>(self, f: F) -> R { #[inline] #[target_feature(enable = "fxsr,sse,sse2")] diff --git a/fearless_simd/src/generated/sse4_2.rs b/fearless_simd/src/generated/sse4_2.rs index 2c7b661b..63be2dfd 100644 --- a/fearless_simd/src/generated/sse4_2.rs +++ b/fearless_simd/src/generated/sse4_2.rs @@ -181,7 +181,7 @@ impl Simd for Sse4_2 { fn level(self) -> Level { Level::Sse4_2(self) } - #[inline(always)] + #[inline] fn vectorize R, R>(self, f: F) -> R { #[inline] #[target_feature(enable = "fxsr,sse4.2,cmpxchg16b,popcnt")] diff --git a/fearless_simd/src/generated/wasm.rs b/fearless_simd/src/generated/wasm.rs index b557912d..16335d4e 100644 --- a/fearless_simd/src/generated/wasm.rs +++ b/fearless_simd/src/generated/wasm.rs @@ -192,7 +192,7 @@ impl Simd for WasmSimd128 { fn level(self) -> Level { Level::WasmSimd128(self) } - #[inline(always)] + #[inline] fn vectorize R, R>(self, f: F) -> R { #[inline] fn vectorize_inner R, R>(f: F) -> R { diff --git a/fearless_simd_gen/src/level.rs b/fearless_simd_gen/src/level.rs index 51231bda..1586cf6a 100644 --- a/fearless_simd_gen/src/level.rs +++ b/fearless_simd_gen/src/level.rs @@ -207,7 +207,7 @@ pub(crate) trait Level { #level_body } - #[inline(always)] + #[inline] fn vectorize R, R>(self, f: F) -> R { #vectorize_body } From 3cfe9879246898e10db6f40fd4fd5c4040f960b6 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 4 Sep 2026 10:30:25 +0100 Subject: [PATCH 10/20] add a comment on #[inline] vs #[inline(always)] distinction in vectorize() --- fearless_simd_gen/src/level.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/fearless_simd_gen/src/level.rs b/fearless_simd_gen/src/level.rs index 1586cf6a..175b485d 100644 --- a/fearless_simd_gen/src/level.rs +++ b/fearless_simd_gen/src/level.rs @@ -207,6 +207,18 @@ pub(crate) trait Level { #level_body } + // We use #[inline] rather than #[inline(always)] deliberately. + // + // Using #[inline(always)] results in both #vectorize_body + // and the outer function always being inlined, + // despite #vectorize_body carrying a weaker #[inline], + // causing extreme code bloat. + // + // The exact cause is difficult to determine with certainty, + // but it looks as if the LLVM inliner walks the call tree + // from the leaves to the root, so it first inlines + // #vectorize_body into vectorize() and then feels bound + // by the outer #[inline(always)]. #[inline] fn vectorize R, R>(self, f: F) -> R { #vectorize_body From bec230b916018802a699a2436eea699455f78562 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Tue, 8 Sep 2026 08:24:00 +0100 Subject: [PATCH 11/20] Do not document the macro as experimental --- CHANGELOG.md | 4 ++-- README.md | 4 ++-- fearless_simd/README.md | 2 +- fearless_simd/src/generated/simd_trait.rs | 4 ++-- fearless_simd/src/lib.rs | 2 +- fearless_simd/src/macros.rs | 2 +- fearless_simd_gen/src/mk_simd_trait.rs | 4 ++-- fearless_simd_macros/README.md | 16 ++++++---------- 8 files changed, 17 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bf184ac..691f0f74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,12 +18,12 @@ You can find its changes [documented below](#070-2026-08-11). - Added `mul_add_precise` and `mul_sub_precise` for floating-point vectors. They guarantee the infinite-precision product-plus-add rounded once, including on SIMD levels without hardware fused multiply-add instructions. They are not susceptible to the [bug](https://github.com/rust-lang/compiler-builtins/issues/1262) in Rust standard library, `std::simd` and musl libc that causes incorrect rounding for subnormal results. SSE4.2 gets SIMD emulation of these operations for better performance. ([#323][], [#324][] by [@Shnatsel][]) - Documented the storage representation of the SIMD vector types. The documented representation will not change without a semver major version change. - Added `TryFrom` bounds to `SimdIntElement`, allowing attempted conversion from all primitive integer types. -- Added the separately versioned, experimental `fearless_simd_macros` companion crate. Its `#[simd]` attribute wraps SIMD-generic function bodies using the public `Simd::vectorize` API, while the core `fearless_simd` crate remains dependency-free. +- Added the separately versioned `fearless_simd_macros` companion crate. Its `#[simd]` attribute wraps SIMD-generic function bodies using the public `Simd::vectorize` API, while the core `fearless_simd` crate remains dependency-free. ### Changed - Breaking change: `SimdBase::as_array` now borrows the vector and returns an array reference, while owned extraction has moved to `to_array`. The old `as_array_ref` and `as_array_mut` methods have been replaced by `as_array` and `as_mut_array`, matching the `std::simd` API. -- `Simd::vectorize` is now inline-friendly on every backend. It retains a target-feature boundary when one is required, but no longer attempts to guarantee an outlined machine-code boundary. Documentation now recommends the optional `#[simd]` attribute for ergonomics and shows direct `vectorize` calls as the dependency-free equivalent. +- Documentation now recommends the optional `#[simd]` attribute for ergonomics and shows direct `vectorize` calls as the dependency-free equivalent. ## [0.7.0][] (2026-08-11) diff --git a/README.md b/README.md index 4275b39a..2145dc33 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ For now, prefer updating the package-level readmes, e.g. fearless_simd/README.md Full details of how to use Fearless SIMD can be found in the -[core package's README](./fearless_simd/README.md). The optional, experimental `#[simd]` +[core package's README](./fearless_simd/README.md). The optional `#[simd]` attribute is provided by the separately versioned [`fearless_simd_macros` companion package](./fearless_simd_macros/README.md); the core package does not depend on it. @@ -40,7 +40,7 @@ The blog post [Safe SIMD in Rust, even on the inside] describes the design and h This repository contains two packages published to crates.io: - `fearless_simd`: The zero-dependency core library, including the examples. -- `fearless_simd_macros`: A separately versioned, experimental companion crate providing the optional `#[simd]` procedural macro. +- `fearless_simd_macros`: A separately versioned companion crate providing the optional `#[simd]` procedural macro. The remaining packages are development tools and tests: diff --git a/fearless_simd/README.md b/fearless_simd/README.md index f4436663..8b8af620 100644 --- a/fearless_simd/README.md +++ b/fearless_simd/README.md @@ -48,7 +48,7 @@ The core crate has zero dependencies, safe public APIs, and ## Automatic vectorization -The easiest way to define a SIMD-generic function is the experimental +The easiest way to define a SIMD-generic function is the [`#[simd]`](https://docs.rs/fearless_simd_macros/latest/fearless_simd_macros/attr.simd.html) attribute from the separately versioned `fearless_simd_macros` crate. The companion macro crate is optional: `fearless_simd` does not depend on it, so users of only the core API do not diff --git a/fearless_simd/src/generated/simd_trait.rs b/fearless_simd/src/generated/simd_trait.rs index 314022ab..14a192f6 100644 --- a/fearless_simd/src/generated/simd_trait.rs +++ b/fearless_simd/src/generated/simd_trait.rs @@ -183,9 +183,9 @@ pub trait Simd: #[doc = r" itself need an `#[inline(always)]` annotation; a target-feature boundary is retained"] #[doc = r" whenever one is required."] #[doc = r""] - #[doc = r" The experimental [`#[simd]`](https://docs.rs/fearless_simd_macros/latest/fearless_simd_macros/attr.simd.html)"] + #[doc = r" The [`#[simd]`](https://docs.rs/fearless_simd_macros/latest/fearless_simd_macros/attr.simd.html)"] #[doc = r" attribute from the separate `fearless_simd_macros` crate performs this wrapping"] - #[doc = r" automatically. Calling `vectorize()` directly is the dependency-free equivalent."] + #[doc = r" automatically. Calling `vectorize()` manually is the dependency-free equivalent."] #[doc = r""] #[doc = r" # Example"] #[doc = r""] diff --git a/fearless_simd/src/lib.rs b/fearless_simd/src/lib.rs index d70ae555..81c138a2 100644 --- a/fearless_simd/src/lib.rs +++ b/fearless_simd/src/lib.rs @@ -15,7 +15,7 @@ //! //! # Automatic vectorization //! -//! The easiest way to define a SIMD-generic function is the experimental +//! The easiest way to define a SIMD-generic function is the //! [`#[simd]`](https://docs.rs/fearless_simd_macros/latest/fearless_simd_macros/attr.simd.html) //! attribute from the separately versioned `fearless_simd_macros` crate. The companion macro //! crate is optional: `fearless_simd` does not depend on it, so users of only the core API do not diff --git a/fearless_simd/src/macros.rs b/fearless_simd/src/macros.rs index b2eb0355..688042b2 100644 --- a/fearless_simd/src/macros.rs +++ b/fearless_simd/src/macros.rs @@ -21,7 +21,7 @@ /// For clarity, it will only be executed once per execution of `dispatch`. /// /// To guarantee target-feature-specific code generation, functions called within the operation -/// should either establish their own context with [`Simd::vectorize`], use the experimental +/// should either establish their own context with [`Simd::vectorize`], use the /// [`#[simd]`](https://docs.rs/fearless_simd_macros/latest/fearless_simd_macros/attr.simd.html) /// attribute, or be `#[inline(always)]` so that they are incorporated into this operation. /// diff --git a/fearless_simd_gen/src/mk_simd_trait.rs b/fearless_simd_gen/src/mk_simd_trait.rs index 54492bdc..368f9531 100644 --- a/fearless_simd_gen/src/mk_simd_trait.rs +++ b/fearless_simd_gen/src/mk_simd_trait.rs @@ -156,9 +156,9 @@ pub(crate) fn mk_simd_trait() -> TokenStream { /// itself need an `#[inline(always)]` annotation; a target-feature boundary is retained /// whenever one is required. /// - /// The experimental [`#[simd]`](https://docs.rs/fearless_simd_macros/latest/fearless_simd_macros/attr.simd.html) + /// The [`#[simd]`](https://docs.rs/fearless_simd_macros/latest/fearless_simd_macros/attr.simd.html) /// attribute from the separate `fearless_simd_macros` crate performs this wrapping - /// automatically. Calling `vectorize()` directly is the dependency-free equivalent. + /// automatically. Calling `vectorize()` manually is the dependency-free equivalent. /// /// # Example /// diff --git a/fearless_simd_macros/README.md b/fearless_simd_macros/README.md index f51e1ba2..aabef939 100644 --- a/fearless_simd_macros/README.md +++ b/fearless_simd_macros/README.md @@ -1,24 +1,20 @@ # Fearless SIMD macros -This crate provides the experimental `#[simd]` attribute for +This crate provides the `#[simd]` attribute for [`fearless_simd`](https://crates.io/crates/fearless_simd). It is versioned separately so that the macro can evolve without adding a procedural-macro dependency to `fearless_simd` itself. -The macro currently targets the inline-friendly `vectorize` implementation on -the main branch. Until the next `fearless_simd` release containing that -implementation and the first macro release are published, use both packages -from Git: +Add both packages from crates.io to your `Cargo.toml`: ```toml [dependencies] -fearless_simd = { git = "https://github.com/linebender/fearless_simd" } -fearless_simd_macros = { git = "https://github.com/linebender/fearless_simd" } +fearless_simd = "1.0" +fearless_simd_macros = "0.1" ``` -`fearless_simd_macros` 0.1.0 must be published only after that core release; -this example should then use the released core version and -`fearless_simd_macros = "0.1"`. +The macro also works with earlier `fearless_simd` versions that provide +`Simd::vectorize`. Then apply `#[simd]` to a function whose first ordinary parameter is its SIMD token: From 422367aa9d0f8439f32eaf14a533c847277b402e Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Tue, 8 Sep 2026 08:29:52 +0100 Subject: [PATCH 12/20] Note the possible use of #[simd] in examples --- fearless_simd/examples/disable_avx2_for_one_function.rs | 2 +- fearless_simd/examples/sigmoid.rs | 2 +- fearless_simd/examples/srgb.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fearless_simd/examples/disable_avx2_for_one_function.rs b/fearless_simd/examples/disable_avx2_for_one_function.rs index db91bc27..d83e9f5a 100644 --- a/fearless_simd/examples/disable_avx2_for_one_function.rs +++ b/fearless_simd/examples/disable_avx2_for_one_function.rs @@ -7,7 +7,7 @@ use fearless_simd::{Level, dispatch, prelude::*}; -#[inline(always)] +#[inline(always)] // or #[simd], either works fn disable_avx2(simd: S, x: &[f32], out: &mut [f32]) { let level = simd.level(); match level { diff --git a/fearless_simd/examples/sigmoid.rs b/fearless_simd/examples/sigmoid.rs index 229665a2..d468f0de 100644 --- a/fearless_simd/examples/sigmoid.rs +++ b/fearless_simd/examples/sigmoid.rs @@ -8,7 +8,7 @@ use fearless_simd::{Level, dispatch, prelude::*}; -#[inline(always)] +#[inline(always)] // or #[simd], either works fn sigmoid(simd: S, x: &[f32], out: &mut [f32]) { let n = S::f32s::N; for (x, y) in x.chunks_exact(n).zip(out.chunks_exact_mut(n)) { diff --git a/fearless_simd/examples/srgb.rs b/fearless_simd/examples/srgb.rs index d3508ed7..e5344d5f 100644 --- a/fearless_simd/examples/srgb.rs +++ b/fearless_simd/examples/srgb.rs @@ -45,7 +45,7 @@ fearless_simd::kernel!( /// /// This helper shows how portable SIMD code can opportunistically call /// target-specific kernels while still providing a fallback for every backend. -#[inline(always)] +#[inline(always)] // or #[simd], either works fn copy_alpha(a: f32x4, b: f32x4) -> f32x4 { #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] if let Some(sse4_2) = a.simd.level().as_sse4_2() { @@ -63,7 +63,7 @@ fn copy_alpha(a: f32x4, b: f32x4) -> f32x4 { } /// Approximate the linear-RGB to sRGB transfer curve for RGB, preserving alpha. -#[inline(always)] +#[inline(always)] // or #[simd], either works fn to_srgb(simd: S, rgba: [f32; 4]) -> [f32; 4] { let v: f32x4 = rgba.simd_into(simd); let vabs = v.abs(); From 3a8dd8d3720fca82c66d417a7edc503b082631eb Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Tue, 8 Sep 2026 09:29:31 +0100 Subject: [PATCH 13/20] Correctly handle functions returning `impl Trait` --- fearless_simd_macros/Cargo.toml | 2 +- fearless_simd_macros/README.md | 4 +- fearless_simd_macros/src/lib.rs | 24 +++++- .../tests/ui/simd/pass/return_types.rs | 83 +++++++++++++++++++ 4 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 fearless_simd_tests/tests/ui/simd/pass/return_types.rs diff --git a/fearless_simd_macros/Cargo.toml b/fearless_simd_macros/Cargo.toml index d5e82349..be34c687 100644 --- a/fearless_simd_macros/Cargo.toml +++ b/fearless_simd_macros/Cargo.toml @@ -17,7 +17,7 @@ proc-macro = true [dependencies] proc-macro2 = { workspace = true } quote = { workspace = true } -syn = { workspace = true } +syn = { workspace = true, features = ["fold"] } [lints] workspace = true diff --git a/fearless_simd_macros/README.md b/fearless_simd_macros/README.md index aabef939..618d47a1 100644 --- a/fearless_simd_macros/README.md +++ b/fearless_simd_macros/README.md @@ -59,7 +59,9 @@ outer wrapper; their effects are not transferred to the generated closure or the target-feature helper that executes it. The closure and `vectorize` call are tail expressions, so the original body's -value and type checking are preserved. +value is preserved. The closure also receives the function's declared return +type to preserve return-value coercions. Any `impl Trait` within that annotation +is replaced with `_` for inference; the function's signature remains unchanged. ## Accepted functions diff --git a/fearless_simd_macros/src/lib.rs b/fearless_simd_macros/src/lib.rs index cada1184..023ad268 100644 --- a/fearless_simd_macros/src/lib.rs +++ b/fearless_simd_macros/src/lib.rs @@ -7,6 +7,7 @@ use core::mem; use proc_macro::TokenStream; use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use quote::quote; +use syn::fold::{self, Fold}; use syn::{AttrStyle, Attribute, FnArg, ItemFn, Pat, PatIdent, Result}; /// Run a SIMD-generic function body with the token's target features enabled. @@ -39,6 +40,11 @@ fn expand(args: TokenStream2, item: TokenStream2) -> Result { let token = simd_token(&mut function)?; let original_statements = mem::take(&mut function.block.stmts); + // Give the closure the same expected return type so branch and early-return + // coercions happen inside its body. Closures cannot name `impl Trait`, so + // infer those parts while preserving the surrounding type structure. + let output = &function.sig.output; + let closure_output = InferImplTrait.fold_return_type(syn::parse_quote!(#output)); // Inner function attributes are held in function.attrs by Syn. Leaving // them there keeps them at the beginning of the outer function body, @@ -49,7 +55,7 @@ fn expand(args: TokenStream2, item: TokenStream2) -> Result { let vectorize_call: syn::Expr = syn::parse_quote! { #token.vectorize( #[inline(always)] - || { #(#original_statements)* } + || #closure_output { #(#original_statements)* } ) }; function @@ -60,6 +66,17 @@ fn expand(args: TokenStream2, item: TokenStream2) -> Result { Ok(quote!(#function)) } +struct InferImplTrait; + +impl Fold for InferImplTrait { + fn fold_type(&mut self, ty: syn::Type) -> syn::Type { + match ty { + syn::Type::ImplTrait(_) => syn::parse_quote!(_), + ty => fold::fold_type(self, ty), + } + } +} + fn reject_unsupported_signature(function: &ItemFn) -> Result<()> { if let Some(asyncness) = &function.sig.asyncness { return Err(syn::Error::new( @@ -258,7 +275,10 @@ mod tests { panic!("vectorize argument should be a closure"); }; - assert!(matches!(closure.output, syn::ReturnType::Default)); + assert_eq!( + closure.output.to_token_stream().to_string(), + parsed.sig.output.to_token_stream().to_string() + ); } } diff --git a/fearless_simd_tests/tests/ui/simd/pass/return_types.rs b/fearless_simd_tests/tests/ui/simd/pass/return_types.rs new file mode 100644 index 00000000..4704d9e2 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/pass/return_types.rs @@ -0,0 +1,83 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::fmt::Display; + +use fearless_simd::{Fallback, Simd}; +use fearless_simd_macros::simd; + +#[simd] +fn select_slice(_: S, first: bool) -> &'static [u32] { + if first { &[1, 2] } else { &[3] } +} + +#[simd] +fn select_box(_: S, first: bool) -> Box { + if first { + return Box::new(1u32); + } + Box::new(2u64) +} + +#[simd] +fn nested_opaque(_: S, first: bool) -> (impl Display, Box) { + if first { + (1, Box::new(2u32)) + } else { + (3, Box::new(4u64)) + } +} + +#[simd] +fn opaque_result(_: S, fail: bool) -> Result, &'static str> { + if fail { + return Err("failed"); + } + Ok([1, 2].into_iter()) +} + +#[simd] +fn opaque_iterator(_: S) -> impl Iterator { + [1, 2].into_iter() +} + +#[simd] +fn borrowed(_: S, values: &mut [u32; 2]) -> &mut [u32] { + values +} + +#[simd] +fn explicit_unit(_: S, value: &mut u32) -> () { + *value += 1; +} + +#[simd] +fn diverging(_: S) -> ! { + panic!("never returns") +} + +fn main() { + let fallback = Fallback::new(); + assert_eq!(select_slice(fallback, true), &[1, 2]); + assert_eq!(select_slice(fallback, false), &[3]); + assert_eq!(select_box(fallback, true).to_string(), "1"); + assert_eq!(select_box(fallback, false).to_string(), "2"); + let (opaque, boxed) = nested_opaque(fallback, true); + assert_eq!(opaque.to_string(), "1"); + assert_eq!(boxed.to_string(), "2"); + let (opaque, boxed) = nested_opaque(fallback, false); + assert_eq!(opaque.to_string(), "3"); + assert_eq!(boxed.to_string(), "4"); + assert_eq!( + opaque_result(fallback, false).unwrap().collect::>(), + [1, 2] + ); + assert_eq!(opaque_result(fallback, true).err(), Some("failed")); + assert_eq!(opaque_iterator(fallback).collect::>(), [1, 2]); + let mut values = [1, 2]; + borrowed(fallback, &mut values)[0] = 3; + assert_eq!(values, [3, 2]); + explicit_unit(fallback, &mut values[0]); + assert_eq!(values, [4, 2]); + let _: fn(Fallback) -> ! = diverging; +} From 610df2839134caa0a8e0bf6f4ec49122422f1713 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 10 Sep 2026 13:58:35 +0100 Subject: [PATCH 14/20] Avoid closure overhead causing stack spills --- CHANGELOG.md | 2 +- Cargo.lock | 25 ++++ fearless_simd/README.md | 5 +- fearless_simd/src/generated/simd_trait.rs | 5 +- fearless_simd/src/kernel_macros.rs | 3 + fearless_simd/src/lib.rs | 5 +- fearless_simd_gen/src/mk_simd_trait.rs | 5 +- fearless_simd_macros/Cargo.toml | 1 + fearless_simd_macros/README.md | 58 ++++++--- fearless_simd_macros/src/lib.rs | 134 +++++++++++++------ fearless_simd_tests/tests/simd_macro.rs | 149 +++++++++++++++++++++- fearless_simd_tests/tests/simd_ui.rs | 13 ++ 12 files changed, 334 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 691f0f74..ec0bb690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ You can find its changes [documented below](#070-2026-08-11). - Added `mul_add_precise` and `mul_sub_precise` for floating-point vectors. They guarantee the infinite-precision product-plus-add rounded once, including on SIMD levels without hardware fused multiply-add instructions. They are not susceptible to the [bug](https://github.com/rust-lang/compiler-builtins/issues/1262) in Rust standard library, `std::simd` and musl libc that causes incorrect rounding for subnormal results. SSE4.2 gets SIMD emulation of these operations for better performance. ([#323][], [#324][] by [@Shnatsel][]) - Documented the storage representation of the SIMD vector types. The documented representation will not change without a semver major version change. - Added `TryFrom` bounds to `SimdIntElement`, allowing attempted conversion from all primitive integer types. -- Added the separately versioned `fearless_simd_macros` companion crate. Its `#[simd]` attribute wraps SIMD-generic function bodies using the public `Simd::vectorize` API, while the core `fearless_simd` crate remains dependency-free. +- Added the separately versioned `fearless_simd_macros` companion crate. Its `#[simd]` attribute runs SIMD-generic function bodies in target-feature-enabled helpers, passing ordinary arguments separately to reduce closure overhead, while the core `fearless_simd` crate remains dependency-free. ### Changed diff --git a/Cargo.lock b/Cargo.lock index 54019df2..6d0e9065 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,6 +146,7 @@ dependencies = [ name = "fearless_simd_macros" version = "0.1.0" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "syn 3.0.4", @@ -220,6 +221,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -356,6 +366,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + [[package]] name = "toml_parser" version = "1.1.3+spec-1.1.0" @@ -485,6 +507,9 @@ name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] [[package]] name = "zmij" diff --git a/fearless_simd/README.md b/fearless_simd/README.md index 8b8af620..be606a35 100644 --- a/fearless_simd/README.md +++ b/fearless_simd/README.md @@ -74,8 +74,9 @@ dispatch!(level, simd => double_u32s(simd, &mut values)); assert_eq!(values, [2, 4, 6, 8, 10]); ``` -The attribute uses the public [`Simd::vectorize`] API. You can call it directly to get the same -behavior without depending on the macro crate: +The attribute generates helpers that pass ordinary arguments separately into a SIMD-enabled +context. You can also establish that context directly with [`Simd::vectorize`] without +depending on the macro crate, though captured arguments may require extra memory traffic: ```rust use fearless_simd::{dispatch, Level, Simd}; diff --git a/fearless_simd/src/generated/simd_trait.rs b/fearless_simd/src/generated/simd_trait.rs index 14a192f6..3fde201f 100644 --- a/fearless_simd/src/generated/simd_trait.rs +++ b/fearless_simd/src/generated/simd_trait.rs @@ -184,8 +184,9 @@ pub trait Simd: #[doc = r" whenever one is required."] #[doc = r""] #[doc = r" The [`#[simd]`](https://docs.rs/fearless_simd_macros/latest/fearless_simd_macros/attr.simd.html)"] - #[doc = r" attribute from the separate `fearless_simd_macros` crate performs this wrapping"] - #[doc = r" automatically. Calling `vectorize()` manually is the dependency-free equivalent."] + #[doc = r" attribute from the separate `fearless_simd_macros` crate establishes this context"] + #[doc = r" automatically, passing ordinary function arguments separately to reduce capture"] + #[doc = r" overhead. Calling `vectorize()` manually avoids the procedural-macro dependency."] #[doc = r""] #[doc = r" # Example"] #[doc = r""] diff --git a/fearless_simd/src/kernel_macros.rs b/fearless_simd/src/kernel_macros.rs index 06fe568b..ae7e5525 100644 --- a/fearless_simd/src/kernel_macros.rs +++ b/fearless_simd/src/kernel_macros.rs @@ -145,6 +145,9 @@ macro_rules! __fearless_simd_kernel_cfg { }; } +// Internal interface for `kernel!` and the argument-taking +// `#[simd]` expansion in `fearless_simd_macros`. Keep the `(level, item)` syntax +// and support for generic function items compatible with that consumer. #[doc(hidden)] #[macro_export] macro_rules! __fearless_simd_kernel_target_fn { diff --git a/fearless_simd/src/lib.rs b/fearless_simd/src/lib.rs index 81c138a2..bb2232f1 100644 --- a/fearless_simd/src/lib.rs +++ b/fearless_simd/src/lib.rs @@ -41,8 +41,9 @@ //! assert_eq!(values, [2, 4, 6, 8, 10]); //! ``` //! -//! The attribute uses the public [`Simd::vectorize`] API. You can call it directly to get the same -//! behavior without depending on the macro crate: +//! The attribute generates helpers that pass ordinary arguments separately into a SIMD-enabled +//! context. You can also establish that context directly with [`Simd::vectorize`] without +//! depending on the macro crate, though captured arguments may require extra memory traffic: //! //! ```rust //! use fearless_simd::{dispatch, Level, Simd}; diff --git a/fearless_simd_gen/src/mk_simd_trait.rs b/fearless_simd_gen/src/mk_simd_trait.rs index 368f9531..7871013e 100644 --- a/fearless_simd_gen/src/mk_simd_trait.rs +++ b/fearless_simd_gen/src/mk_simd_trait.rs @@ -157,8 +157,9 @@ pub(crate) fn mk_simd_trait() -> TokenStream { /// whenever one is required. /// /// The [`#[simd]`](https://docs.rs/fearless_simd_macros/latest/fearless_simd_macros/attr.simd.html) - /// attribute from the separate `fearless_simd_macros` crate performs this wrapping - /// automatically. Calling `vectorize()` manually is the dependency-free equivalent. + /// attribute from the separate `fearless_simd_macros` crate establishes this context + /// automatically, passing ordinary function arguments separately to reduce capture + /// overhead. Calling `vectorize()` manually avoids the procedural-macro dependency. /// /// # Example /// diff --git a/fearless_simd_macros/Cargo.toml b/fearless_simd_macros/Cargo.toml index be34c687..fcea74e9 100644 --- a/fearless_simd_macros/Cargo.toml +++ b/fearless_simd_macros/Cargo.toml @@ -15,6 +15,7 @@ readme = "README.md" proc-macro = true [dependencies] +proc-macro-crate = "3.5.0" proc-macro2 = { workspace = true } quote = { workspace = true } syn = { workspace = true, features = ["fold"] } diff --git a/fearless_simd_macros/README.md b/fearless_simd_macros/README.md index 618d47a1..31f438b0 100644 --- a/fearless_simd_macros/README.md +++ b/fearless_simd_macros/README.md @@ -13,8 +13,10 @@ fearless_simd = "1.0" fearless_simd_macros = "0.1" ``` -The macro also works with earlier `fearless_simd` versions that provide -`Simd::vectorize`. +The consumer must declare `fearless_simd` as a direct dependency; renamed Cargo +dependencies are supported. The macro uses the library's internal +`__fearless_simd_kernel_target_fn!` helper, so `Simd::vectorize` alone is no +longer sufficient for compatibility with older library versions. Then apply `#[simd]` to a function whose first ordinary parameter is its SIMD token: @@ -36,32 +38,45 @@ fn double_u32s(simd: S, values: &mut [u32]) { } ``` -Conceptually, the macro expands the body to: +Conceptually, the macro passes the body and its arguments to a generated +dispatcher: ```rust,ignore fn double_u32s(simd: S, values: &mut [u32]) { - simd.vectorize( + dispatcher.call( + simd, + simd, + values, #[inline(always)] - || { + |simd, values| { // Original body. }, ) } ``` +Here `dispatcher` represents generated helpers that select the token's backend +and enable its target features. The first token selects the backend; the +remaining values become arguments to the body. Each argument has a separate +helper parameter, allowing the compiler to pass it in registers even when the +helper remains out of line. There is no fixed argument-count limit. + The attributed closure ensures that the original body is inlined into the -target-feature-enabled function provided by `Simd::vectorize`. The macro does -not add an `#[inline]` attribute to the annotated function. Any existing -`#[inline]`, documentation, lint, conditional-compilation, or other function -attributes remain on that function unchanged. +target-feature-enabled helper. The helpers use ordinary `#[inline]` so large +bodies can remain shared between callers. The macro does not add an `#[inline]` +attribute to the annotated function. Any existing `#[inline]`, documentation, +lint, conditional-compilation, or other function attributes remain on that +function unchanged. Code-placement attributes such as `#[cold]` therefore continue to describe the outer wrapper; their effects are not transferred to the generated closure or the target-feature helper that executes it. -The closure and `vectorize` call are tail expressions, so the original body's +The closure and dispatcher call are tail expressions, so the original body's value is preserved. The closure also receives the function's declared return type to preserve return-value coercions. Any `impl Trait` within that annotation -is replaced with `_` for inference; the function's signature remains unchanged. +is replaced with `_` for inference; the function's argument and return types +remain unchanged. Ordinary parameter bindings and their lint attributes move +into the closure, and the outer parameters receive private names. ## Accepted functions @@ -91,22 +106,27 @@ diagnose it. ## Execution boundaries and captures -Only work performed while the function body is executing is covered by -`vectorize`. Code inside a returned future, closure, or lazy iterator runs +Only work performed while the function body is executing is covered by the +SIMD context. Code inside a returned future, closure, or lazy iterator runs later and is not covered. Named helper functions do not inherit the enabled target features; make them inlineable or annotate their own SIMD-generic body. -Recursive calls enter `vectorize` again. +Recursive calls enter the dispatcher again. -The original body becomes a non-`move` `FnOnce` closure. Rust infers each -capture mode from how the body uses its parameters. As with any closure +The original body becomes an always-inline `FnOnce` closure with explicit +parameters. Receivers and parameters carrying `#[cfg]` or `#[cfg_attr]` remain +captures, preserving their existing semantics and uses inside nested macros. +These captures can still require memory when a helper remains out of line. +Rust infers their capture modes from how the body uses them. As with any closure conversion, the destruction order of captured values is not a stable substitute for function-parameter destruction order. Avoid relying on the relative drop order of by-value parameters with observable destructors in a `#[simd]` function. -This procedural macro intentionally has no dependency on `fearless_simd`. -The selected token's type must therefore make the `vectorize` trait method -available, normally through an `S: Simd` bound. +The selected token must implement `fearless_simd::Simd`, normally through an +`S: Simd` bound. The macro generates paths to the consumer's library dependency; +the procedural-macro crate itself does not depend on `fearless_simd`. +Unknown future backends retain the existing `Simd::vectorize` path until the +macro is updated to generate helpers for them. ## Minimum supported Rust version diff --git a/fearless_simd_macros/src/lib.rs b/fearless_simd_macros/src/lib.rs index 023ad268..1824a9ab 100644 --- a/fearless_simd_macros/src/lib.rs +++ b/fearless_simd_macros/src/lib.rs @@ -8,7 +8,9 @@ use proc_macro::TokenStream; use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use quote::quote; use syn::fold::{self, Fold}; -use syn::{AttrStyle, Attribute, FnArg, ItemFn, Pat, PatIdent, Result}; +use syn::{AttrStyle, Attribute, FnArg, ItemFn, Pat, Result}; + +mod dispatch; /// Run a SIMD-generic function body with the token's target features enabled. /// @@ -17,12 +19,27 @@ use syn::{AttrStyle, Attribute, FnArg, ItemFn, Pat, PatIdent, Result}; /// expansion, supported function forms, and semantic caveats. #[proc_macro_attribute] pub fn simd(args: TokenStream, item: TokenStream) -> TokenStream { - expand(args.into(), item.into()) + library_path() + .and_then(|library| expand(args.into(), item.into(), &library)) .unwrap_or_else(syn::Error::into_compile_error) .into() } -fn expand(args: TokenStream2, item: TokenStream2) -> Result { +fn library_path() -> Result { + match proc_macro_crate::crate_name("fearless_simd") { + Ok(proc_macro_crate::FoundCrate::Itself) => Ok(syn::parse_quote!(crate)), + Ok(proc_macro_crate::FoundCrate::Name(name)) => { + let name = Ident::new(&name, Span::call_site()); + Ok(syn::parse_quote!(::#name)) + } + Err(error) => Err(syn::Error::new( + Span::call_site(), + format!("`#[simd]` requires a `fearless_simd` dependency: {error}"), + )), + } +} + +fn expand(args: TokenStream2, item: TokenStream2, library: &syn::Path) -> Result { if !args.is_empty() { return Err(syn::Error::new_spanned( args, @@ -38,7 +55,10 @@ fn expand(args: TokenStream2, item: TokenStream2) -> Result { reject_unsupported_signature(&function)?; reject_unsupported_attributes(&function.attrs)?; - let token = simd_token(&mut function)?; + let original_token = validate_simd_token(&function)?; + // The old expansion used this binding for vectorize even when the body + // did not use it. Preserve that usage for unused-variable lint purposes. + let use_token = original_token.map(|token| quote!(let _ = #token;)); let original_statements = mem::take(&mut function.block.stmts); // Give the closure the same expected return type so branch and early-return // coercions happen inside its body. Closures cannot name `impl Trait`, so @@ -46,22 +66,53 @@ fn expand(args: TokenStream2, item: TokenStream2) -> Result { let output = &function.sig.output; let closure_output = InferImplTrait.fold_return_type(syn::parse_quote!(#output)); + let mut parameters = Vec::new(); + let mut arguments = Vec::new(); + let mut argument_types = Vec::new(); + for (index, argument) in function.sig.inputs.iter_mut().enumerate() { + let FnArg::Typed(argument) = argument else { + // Keep `self` captured so its uses, including inside nested macros, + // retain their original meaning without rewriting the body. + continue; + }; + if has_conditional_attributes(&argument.attrs) { + // FnOnce's argument types cannot carry cfg attributes. Preserve + // conditional parameters as captures, just as in the old expansion. + continue; + } + let name = Ident::new(&format!("__fearless_argument_{index}"), Span::mixed_site()); + let ty = Ident::new(&format!("__FearlessArgument{index}"), Span::mixed_site()); + let pattern = mem::replace(&mut argument.pat, Box::new(syn::parse_quote!(#name))); + // Move lint attributes with the original binding. Duplicating `expect` + // on the now-used outer parameter would leave an unfulfilled expectation. + let attrs = mem::take(&mut argument.attrs); + parameters.push(quote!(#(#attrs)* #pattern)); + arguments.push(name); + argument_types.push(ty); + } + // The first typed argument is the validated, unconditional SIMD token. + let token = &arguments[0]; + let dispatcher = dispatch::dispatcher(library, &argument_types, &arguments); + // Inner function attributes are held in function.attrs by Syn. Leaving // them there keeps them at the beginning of the outer function body, // rather than changing their scope by moving them into this closure. // Keep generated wrapper tokens on their normal macro-expansion spans. // Giving the entire call the token parameter's source span makes Clippy's // `semicolon_if_nothing_returned` lint fire on unit-returning functions. - let vectorize_call: syn::Expr = syn::parse_quote! { - #token.vectorize( + // Keep the closure directly in the call, after the arguments: the FnOnce + // bound then infers its parameter types and their borrowed-return lifetimes. + let dispatch_call: syn::Expr = syn::parse_quote! { + (#dispatcher).call( + #token, #(#arguments,)* #[inline(always)] - || #closure_output { #(#original_statements)* } + |#(#parameters),*| #closure_output { #use_token #(#original_statements)* } ) }; function .block .stmts - .push(syn::Stmt::Expr(vectorize_call, None)); + .push(syn::Stmt::Expr(dispatch_call, None)); Ok(quote!(#function)) } @@ -135,11 +186,11 @@ fn is_attribute(attr: &Attribute, name: &str) -> bool { .is_ok_and(|path| path.is_ident(name)) } -fn simd_token(function: &mut ItemFn) -> Result { +fn validate_simd_token(function: &ItemFn) -> Result> { let Some(argument) = function .sig .inputs - .iter_mut() + .iter() .find_map(|argument| match argument { FnArg::Receiver(_) => None, FnArg::Typed(argument) => Some(argument), @@ -153,7 +204,7 @@ fn simd_token(function: &mut ItemFn) -> Result { reject_conditional_attributes(&argument.attrs)?; - match &mut *argument.pat { + match &*argument.pat { Pat::Ident(pattern) => { reject_conditional_attributes(&pattern.attrs)?; if let Some(by_ref) = &pattern.by_ref { @@ -168,20 +219,11 @@ fn simd_token(function: &mut ItemFn) -> Result { "the SIMD token parameter cannot use an `@` subpattern", )); } - Ok(pattern.ident.clone()) + Ok(Some(pattern.ident.clone())) } Pat::Wild(pattern) => { reject_conditional_attributes(&pattern.attrs)?; - let token = Ident::new("__fearless_simd_token", Span::mixed_site()); - let attrs = mem::take(&mut pattern.attrs); - *argument.pat = Pat::Ident(PatIdent { - attrs, - by_ref: None, - mutability: None, - ident: token.clone(), - subpat: None, - }); - Ok(token) + Ok(None) } pattern => Err(syn::Error::new_spanned( pattern, @@ -203,12 +245,24 @@ fn reject_conditional_attributes(attrs: &[Attribute]) -> Result<()> { Ok(()) } +fn has_conditional_attributes(attrs: &[Attribute]) -> bool { + attrs + .iter() + .any(|attr| attr.path().is_ident("cfg") || attr.path().is_ident("cfg_attr")) +} + #[cfg(test)] mod tests { - use super::expand; use quote::{ToTokens, quote}; use syn::{AttrStyle, Expr, ItemFn, Stmt}; + fn expand( + args: proc_macro2::TokenStream, + item: proc_macro2::TokenStream, + ) -> syn::Result { + super::expand(args, item, &syn::parse_quote!(::fearless_simd)) + } + fn expand_ok(item: proc_macro2::TokenStream) -> proc_macro2::TokenStream { expand(proc_macro2::TokenStream::new(), item).expect("macro expansion should succeed") } @@ -231,15 +285,15 @@ mod tests { let Some(Stmt::Expr(Expr::MethodCall(call), None)) = parsed.block.stmts.last() else { panic!("function tail should be a method call"); }; - let Some(Expr::Closure(closure)) = call.args.first() else { - panic!("vectorize argument should be a closure"); + let Some(Expr::Closure(closure)) = call.args.last() else { + panic!("last dispatcher argument should be a closure"); }; let Expr::Block(body) = &*closure.body else { panic!("closure body should be a block"); }; - assert_eq!(call.method, "vectorize"); - assert_eq!(call.args.len(), 1); + assert_eq!(call.args.len(), 5); + assert_eq!(closure.inputs.len(), 3); assert!(closure.capture.is_none()); assert_eq!(closure.attrs.len(), 1); assert!(closure.attrs[0].path().is_ident("inline")); @@ -247,7 +301,7 @@ mod tests { .parse_args() .expect("inline attribute has one identifier argument"); assert_eq!(inline_kind, "always"); - assert_eq!(body.block.stmts.len(), 2); + assert_eq!(body.block.stmts.len(), 3); assert!( parsed .attrs @@ -271,8 +325,8 @@ mod tests { let Some(Stmt::Expr(Expr::MethodCall(call), None)) = parsed.block.stmts.last() else { panic!("unit function tail should be a method call"); }; - let Some(Expr::Closure(closure)) = call.args.first() else { - panic!("vectorize argument should be a closure"); + let Some(Expr::Closure(closure)) = call.args.last() else { + panic!("last dispatcher argument should be a closure"); }; assert_eq!( @@ -327,12 +381,10 @@ mod tests { let inner_attr = text .find("# ! [allow") .expect("inner attribute is retained"); - let call = text - .find("simd . vectorize") - .expect("vectorize call exists"); + let call = text.find("__FearlessDispatch").expect("dispatcher exists"); assert!(inner_attr < call); assert_eq!(text.matches("inline (never)").count(), 1); - assert_eq!(text.matches("inline (always)").count(), 1); + assert_eq!(text.matches("inline (always)").count(), 2); assert!(text.contains("target_feature")); } @@ -346,8 +398,8 @@ mod tests { }); let text = expanded.to_string(); - assert!(text.contains("mut backend : S")); - assert!(text.contains("backend . vectorize")); + assert!(text.contains("__fearless_argument_1 : S")); + assert!(text.contains("| mut backend , value |")); } #[test] @@ -357,9 +409,9 @@ mod tests { }); let text = expanded.to_string(); - assert_eq!(text.matches("__fearless_simd_token").count(), 2); - assert!(text.contains("__fearless_simd_token : S")); - assert!(text.contains("__fearless_simd_token . vectorize")); + assert_eq!(text.matches("__fearless_simd_token").count(), 0); + assert!(text.contains("__fearless_argument_0 : S")); + assert!(text.contains("| _ , value |")); } #[test] @@ -378,7 +430,7 @@ mod tests { .to_string() .matches("__fearless_simd_token") .count(), - 4 + 2 ); } @@ -387,7 +439,7 @@ mod tests { let expanded = expand_ok(quote! { fn operation(&self, simd: S) -> u32 { 42 } }); - assert!(expanded.to_string().contains("simd . vectorize")); + assert!(expanded.to_string().contains("| simd |")); } #[test] diff --git a/fearless_simd_tests/tests/simd_macro.rs b/fearless_simd_tests/tests/simd_macro.rs index f3a0ebc1..212d5e30 100644 --- a/fearless_simd_tests/tests/simd_macro.rs +++ b/fearless_simd_tests/tests/simd_macro.rs @@ -57,7 +57,7 @@ fn simd_attribute_runs_on_every_backend(simd: S) { assert_eq!( values, [2, 4, 6, 8, 10, 12, 14, 16, 18], - "the autovectorized body should execute inside vectorize" + "the autovectorized body should execute inside the SIMD context" ); let sum = add_vectors(simd, [1, 2, 3, 4, 5, 6, 7, 8], [8, 7, 6, 5, 4, 3, 2, 1]); @@ -69,7 +69,7 @@ fn simd_attribute_runs_on_every_backend(simd: S) { assert_eq!( increment_first(simd, &mut values), Ok(()), - "the success path should propagate through vectorize" + "the success path should propagate through the SIMD context" ); assert_eq!(values[0], 3, "the success path should mutate its input"); assert_eq!( @@ -96,3 +96,148 @@ fn simd_attribute_runs_on_every_backend(simd: S) { "the macro should support default trait methods" ); } + +#[simd_test] +fn simd_attribute_preserves_argument_patterns_and_borrows(simd: S) { + #[simd] + fn update<'a, S: Simd>( + _: S, + (ref label, mut value): (String, u32), + output: &'a mut [u32], + tag: impl core::fmt::Display, + ) -> (&'a mut [u32], impl core::fmt::Display) { + assert_eq!(label, "value"); + value += 1; + output[0] = value; + (output, tag) + } + + let mut output = [0, 0]; + { + let (borrowed, tag) = update(simd, ("value".into(), 41), &mut output, "tag"); + assert_eq!( + borrowed, + &[42, 0], + "destructured arguments should update the borrowed output" + ); + borrowed[1] = 7; + assert_eq!( + tag.to_string(), + "tag", + "the opaque argument should be returned intact" + ); + } + assert_eq!( + output, + [42, 7], + "the returned borrow should still refer to the original output" + ); +} + +#[simd_test] +fn simd_attribute_preserves_conditional_arguments(simd: S) { + #[simd] + fn join( + _: S, + #[cfg(test)] prefix: String, + #[cfg(any())] absent: UndefinedType, + #[cfg_attr( + test, + allow( + unused_variables, + reason = "exercise a conditional parameter attribute" + ) + )] + unused: u32, + suffix: String, + ) -> String { + prefix + &suffix + } + + assert_eq!( + join(simd, "first".into(), 0, "last".into()), + "firstlast", + "conditional captures and explicit arguments should both be available" + ); +} + +#[simd_test] +fn simd_attribute_preserves_receiver_and_associated_types(simd: S) { + struct Container(T); + + impl Container { + #[simd] + fn get( + &mut self, + _: S, + tag: impl core::fmt::Display, + ) -> (&mut T, impl FnOnce() -> String) { + let _: Option = None; + (&mut self.0, move || tag.to_string()) + } + } + + trait Identity { + type Item; + + #[simd] + fn identity(&self, _: S, value: Self::Item) -> Self::Item { + value + } + } + + impl Identity for Container { + type Item = String; + } + + let mut container = Container(1); + let (value, tag) = container.get(simd, "tag"); + *value = 2; + assert_eq!(tag(), "tag", "the returned closure should own its argument"); + assert_eq!( + container.0, 2, + "the returned reference should borrow the receiver" + ); + assert_eq!( + container.identity(simd, "owned".into()), + "owned", + "Self::Item should resolve in the original trait scope" + ); +} + +#[simd_test] +fn simd_attribute_preserves_opaque_return_coercions(simd: S) { + #[simd] + fn select( + _: S, + first: bool, + ) -> (impl core::fmt::Display, Box) { + if first { + return (1, Box::new(2_u32)); + } + (3, Box::new(4_u64)) + } + + let (value, boxed) = select(simd, true); + assert_eq!( + value.to_string(), + "1", + "the early-return opaque value should be preserved" + ); + assert_eq!( + boxed.to_string(), + "2", + "the early return should coerce to a trait object" + ); + let (value, boxed) = select(simd, false); + assert_eq!( + value.to_string(), + "3", + "the tail opaque value should be preserved" + ); + assert_eq!( + boxed.to_string(), + "4", + "the tail return should coerce to the same trait object type" + ); +} diff --git a/fearless_simd_tests/tests/simd_ui.rs b/fearless_simd_tests/tests/simd_ui.rs index 117ea5a4..5509f13d 100644 --- a/fearless_simd_tests/tests/simd_ui.rs +++ b/fearless_simd_tests/tests/simd_ui.rs @@ -10,4 +10,17 @@ fn simd_ui() { let tests = trybuild::TestCases::new(); tests.pass("tests/ui/simd/pass/*.rs"); tests.compile_fail("tests/ui/simd/fail/*.rs"); + + let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + let status = std::process::Command::new(env!("CARGO")) + .args(["check", "--offline", "--manifest-path"]) + .arg(manifest_dir.join("tests/ui/simd/renamed/Cargo.toml")) + .arg("--target-dir") + .arg(manifest_dir.join("../target/tests/simd-renamed")) + .status() + .expect("renamed-dependency fixture should start cargo"); + assert!( + status.success(), + "renamed-dependency fixture should compile" + ); } From 8aafa95c4af12b40d44c28cb507a93f0caf9cb28 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 10 Sep 2026 14:24:44 +0100 Subject: [PATCH 15/20] Explain what the hell is going on in the argument rewriting --- fearless_simd_macros/src/lib.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/fearless_simd_macros/src/lib.rs b/fearless_simd_macros/src/lib.rs index 1824a9ab..80064087 100644 --- a/fearless_simd_macros/src/lib.rs +++ b/fearless_simd_macros/src/lib.rs @@ -66,6 +66,20 @@ fn expand(args: TokenStream2, item: TokenStream2, library: &syn::Path) -> Result let output = &function.sig.output; let closure_output = InferImplTrait.fold_return_type(syn::parse_quote!(#output)); + // Wrapping the body in simd.vectorize(|| ...) puts its captured arguments + // into a closure struct. If the target-feature helper remains out of line, + // that struct can be passed through memory, spilling arguments that would + // otherwise fit in registers. Instead, make the body a FnOnce(A0, A1, ...) + // and pass each argument separately through the dispatcher. This preserves + // register passing without forcing large bodies to inline into every caller. + // + // Give outer parameters fresh names so we can forward their whole values, + // even when their original patterns destructure them. Move those patterns + // into the closure's parameters so the body can keep its original bindings. + // Fresh generic argument types let the helpers forward these values without + // having to reproduce the outer function's generics, lifetimes, or Self. + // Keeping one closure body also preserves a single opaque return type when + // the function returns impl Trait. let mut parameters = Vec::new(); let mut arguments = Vec::new(); let mut argument_types = Vec::new(); From 69c2aa63385784e5d1354df2643ba1e53e7578fa Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 10 Sep 2026 14:54:02 +0100 Subject: [PATCH 16/20] Remove dependency on proc-macro-crate to locate fearless_simd. Now that it opens up an opportunity to spoof fearless_simd types, which would be unsound, move the unsafe blocks into a declarative macro inside fearless_simd so that the proc macro emits no unsafe code itself. --- Cargo.lock | 25 ------ fearless_simd/src/kernel_macros.rs | 133 ++++++++++++++++++++++++++++- fearless_simd_macros/Cargo.toml | 1 - fearless_simd_macros/README.md | 31 +++++-- fearless_simd_macros/src/lib.rs | 57 ++++++------- 5 files changed, 180 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d0e9065..54019df2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,7 +146,6 @@ dependencies = [ name = "fearless_simd_macros" version = "0.1.0" dependencies = [ - "proc-macro-crate", "proc-macro2", "quote", "syn 3.0.4", @@ -221,15 +220,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - [[package]] name = "proc-macro2" version = "1.0.95" @@ -366,18 +356,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.25.13+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - [[package]] name = "toml_parser" version = "1.1.3+spec-1.1.0" @@ -507,9 +485,6 @@ name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -dependencies = [ - "memchr", -] [[package]] name = "zmij" diff --git a/fearless_simd/src/kernel_macros.rs b/fearless_simd/src/kernel_macros.rs index ae7e5525..1061324c 100644 --- a/fearless_simd/src/kernel_macros.rs +++ b/fearless_simd/src/kernel_macros.rs @@ -145,9 +145,8 @@ macro_rules! __fearless_simd_kernel_cfg { }; } -// Internal interface for `kernel!` and the argument-taking -// `#[simd]` expansion in `fearless_simd_macros`. Keep the `(level, item)` syntax -// and support for generic function items compatible with that consumer. +// Internal interface for `kernel!` and `__fearless_simd_dispatch!`, which backs +// the argument-taking `#[simd]` expansion in `fearless_simd_macros`. #[doc(hidden)] #[macro_export] macro_rules! __fearless_simd_kernel_target_fn { @@ -185,6 +184,134 @@ macro_rules! __fearless_simd_kernel_target_fn { }; } +// Internal interface for `#[simd]` in `fearless_simd_macros`. Each pair names a +// fresh generic argument type and its corresponding function parameter. Keep +// this protocol compatible with that consumer, including the `.call()` method. +// +// This exported macro must be safe to invoke directly. It only accepts names, +// never caller-supplied token types, bounds, attributes, or code for an unsafe +// context. All safety-critical library paths use `$crate`: even if the proc +// macro's `fearless_simd` path resolves to a re-export, dispatch requires genuine +// proof tokens from the library that defines the target-feature helper. The +// user's closure is passed to the resulting safe method outside this macro. +#[doc(hidden)] +#[macro_export] +macro_rules! __fearless_simd_dispatch { + ($($argument_type:ident => $argument:ident),* $(,)?) => {{ + // Scope generated items to the callee expression, away from the user + // body. Item names do not have local-variable hygiene. Use a method on + // a zero-sized receiver: returning a generic function item from this + // block loses the expected FnOnce signature during closure input + // inference. Method lookup retains it without adding a machine argument. + struct __FearlessDispatch; + impl __FearlessDispatch { + // Do not force inlining: a large body may be shared by several + // callers. Even an out-of-line dispatcher keeps separate arguments. + #[inline] + fn call( + self, simd: S, $($argument: $argument_type,)* f: F, + ) -> R + where F: ::core::ops::FnOnce($($argument_type),*) -> R { + match $crate::Simd::level(simd) { + #[cfg(target_arch = "aarch64")] + $crate::Level::Neon(proof) => { + $crate::__fearless_simd_kernel_target_fn! { + Neon, + #[inline] + fn entry<$($argument_type,)* F, R>( + _: $crate::Neon, $($argument: $argument_type,)* f: F, + ) -> R + where F: ::core::ops::FnOnce($($argument_type),*) -> R { + f($($argument),*) + } + } + // SAFETY: the genuine proof token establishes all the + // target features attached by the library's helper. + unsafe { entry(proof, $($argument,)* f) } + } + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + $crate::Level::Sse2(proof) => { + $crate::__fearless_simd_kernel_target_fn! { + Sse2, + #[inline] + fn entry<$($argument_type,)* F, R>( + _: $crate::Sse2, $($argument: $argument_type,)* f: F, + ) -> R + where F: ::core::ops::FnOnce($($argument_type),*) -> R { + f($($argument),*) + } + } + // SAFETY: the genuine proof token establishes all the + // target features attached by the library's helper. + unsafe { entry(proof, $($argument,)* f) } + } + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + $crate::Level::Sse4_2(proof) => { + $crate::__fearless_simd_kernel_target_fn! { + Sse4_2, + #[inline] + fn entry<$($argument_type,)* F, R>( + _: $crate::Sse4_2, $($argument: $argument_type,)* f: F, + ) -> R + where F: ::core::ops::FnOnce($($argument_type),*) -> R { + f($($argument),*) + } + } + // SAFETY: the genuine proof token establishes all the + // target features attached by the library's helper. + unsafe { entry(proof, $($argument,)* f) } + } + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + $crate::Level::Avx2(proof) => { + $crate::__fearless_simd_kernel_target_fn! { + Avx2, + #[inline] + fn entry<$($argument_type,)* F, R>( + _: $crate::Avx2, $($argument: $argument_type,)* f: F, + ) -> R + where F: ::core::ops::FnOnce($($argument_type),*) -> R { + f($($argument),*) + } + } + // SAFETY: the genuine proof token establishes all the + // target features attached by the library's helper. + unsafe { entry(proof, $($argument,)* f) } + } + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + $crate::Level::Avx512(proof) => { + $crate::__fearless_simd_kernel_target_fn! { + Avx512, + #[inline] + fn entry<$($argument_type,)* F, R>( + _: $crate::Avx512, $($argument: $argument_type,)* f: F, + ) -> R + where F: ::core::ops::FnOnce($($argument_type),*) -> R { + f($($argument),*) + } + } + // SAFETY: the genuine proof token establishes all the + // target features attached by the library's helper. + unsafe { entry(proof, $($argument,)* f) } + } + #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))] + $crate::Level::WasmSimd128(_) => f($($argument),*), + // This public predicate works even when the library omits + // Level::Fallback. Do not inspect downstream Cargo features. + level if level.is_fallback() => f($($argument),*), + // Level is non_exhaustive. Future backends still get their + // own feature context until this macro learns about them. + _ => $crate::Simd::vectorize( + simd, + #[inline(always)] + move || f($($argument),*), + ), + } + } + } + __FearlessDispatch + }}; +} + /// The implementation protocol must not accept caller-supplied token types or /// target-feature annotations. /// diff --git a/fearless_simd_macros/Cargo.toml b/fearless_simd_macros/Cargo.toml index fcea74e9..be34c687 100644 --- a/fearless_simd_macros/Cargo.toml +++ b/fearless_simd_macros/Cargo.toml @@ -15,7 +15,6 @@ readme = "README.md" proc-macro = true [dependencies] -proc-macro-crate = "3.5.0" proc-macro2 = { workspace = true } quote = { workspace = true } syn = { workspace = true, features = ["fold"] } diff --git a/fearless_simd_macros/README.md b/fearless_simd_macros/README.md index 31f438b0..9b5acfa5 100644 --- a/fearless_simd_macros/README.md +++ b/fearless_simd_macros/README.md @@ -13,10 +13,22 @@ fearless_simd = "1.0" fearless_simd_macros = "0.1" ``` -The consumer must declare `fearless_simd` as a direct dependency; renamed Cargo -dependencies are supported. The macro uses the library's internal -`__fearless_simd_kernel_target_fn!` helper, so `Simd::vectorize` alone is no -longer sufficient for compatibility with older library versions. +The library must be in scope as `fearless_simd` in the module containing the +annotated function. The dependency declaration above makes that name available +automatically. For a renamed Cargo dependency, add an alias in that module: + +```rust,ignore +use simd_backend as fearless_simd; +``` + +A library re-export can be imported with +`use my_facade::simd_backend as fearless_simd;`. For use inside the +`fearless_simd` library itself, write `use crate as fearless_simd;`. +The macro does not inspect Cargo manifests to discover dependency names. + +The library version must provide the internal `__fearless_simd_dispatch!` +helper. `Simd::vectorize` or `__fearless_simd_kernel_target_fn!` alone is not +sufficient for compatibility with older library versions. Then apply `#[simd]` to a function whose first ordinary parameter is its SIMD token: @@ -122,11 +134,14 @@ substitute for function-parameter destruction order. Avoid relying on the relative drop order of by-value parameters with observable destructors in a `#[simd]` function. -The selected token must implement `fearless_simd::Simd`, normally through an -`S: Simd` bound. The macro generates paths to the consumer's library dependency; -the procedural-macro crate itself does not depend on `fearless_simd`. +The selected token must implement the library's `Simd` trait, normally through +an `S: Simd` bound. The procedural macro invokes +`fearless_simd::__fearless_simd_dispatch!`; the procedural-macro crate itself +does not depend on `fearless_simd`. The library helper owns the unsafe calls +and resolves all proof types through `$crate`, so a lookalike module cannot +substitute counterfeit proof tokens when re-exporting that helper. Unknown future backends retain the existing `Simd::vectorize` path until the -macro is updated to generate helpers for them. +library helper is updated to generate entries for them. ## Minimum supported Rust version diff --git a/fearless_simd_macros/src/lib.rs b/fearless_simd_macros/src/lib.rs index 80064087..7d6eee31 100644 --- a/fearless_simd_macros/src/lib.rs +++ b/fearless_simd_macros/src/lib.rs @@ -10,36 +10,21 @@ use quote::quote; use syn::fold::{self, Fold}; use syn::{AttrStyle, Attribute, FnArg, ItemFn, Pat, Result}; -mod dispatch; - /// Run a SIMD-generic function body with the token's target features enabled. /// /// The first typed parameter after an optional `self` receiver is used as the -/// SIMD token. See the [crate-level documentation](crate) for the complete +/// SIMD token. The library must be in scope as `fearless_simd`; for a renamed +/// dependency, import it with `use simd_backend as fearless_simd;` in the +/// containing module. See the [crate-level documentation](crate) for the complete /// expansion, supported function forms, and semantic caveats. #[proc_macro_attribute] pub fn simd(args: TokenStream, item: TokenStream) -> TokenStream { - library_path() - .and_then(|library| expand(args.into(), item.into(), &library)) + expand(args.into(), item.into()) .unwrap_or_else(syn::Error::into_compile_error) .into() } -fn library_path() -> Result { - match proc_macro_crate::crate_name("fearless_simd") { - Ok(proc_macro_crate::FoundCrate::Itself) => Ok(syn::parse_quote!(crate)), - Ok(proc_macro_crate::FoundCrate::Name(name)) => { - let name = Ident::new(&name, Span::call_site()); - Ok(syn::parse_quote!(::#name)) - } - Err(error) => Err(syn::Error::new( - Span::call_site(), - format!("`#[simd]` requires a `fearless_simd` dependency: {error}"), - )), - } -} - -fn expand(args: TokenStream2, item: TokenStream2, library: &syn::Path) -> Result { +fn expand(args: TokenStream2, item: TokenStream2) -> Result { if !args.is_empty() { return Err(syn::Error::new_spanned( args, @@ -106,7 +91,6 @@ fn expand(args: TokenStream2, item: TokenStream2, library: &syn::Path) -> Result } // The first typed argument is the validated, unconditional SIMD token. let token = &arguments[0]; - let dispatcher = dispatch::dispatcher(library, &argument_types, &arguments); // Inner function attributes are held in function.attrs by Syn. Leaving // them there keeps them at the beginning of the outer function body, @@ -116,8 +100,10 @@ fn expand(args: TokenStream2, item: TokenStream2, library: &syn::Path) -> Result // `semicolon_if_nothing_returned` lint fire on unit-returning functions. // Keep the closure directly in the call, after the arguments: the FnOnce // bound then infers its parameter types and their borrowed-return lifetimes. + // The library macro owns the unsafe calls and resolves proof types through + // $crate. A lookalike `fearless_simd` module cannot spoof those proofs. let dispatch_call: syn::Expr = syn::parse_quote! { - (#dispatcher).call( + (fearless_simd::__fearless_simd_dispatch!(#(#argument_types => #arguments),*)).call( #token, #(#arguments,)* #[inline(always)] |#(#parameters),*| #closure_output { #use_token #(#original_statements)* } @@ -270,12 +256,7 @@ mod tests { use quote::{ToTokens, quote}; use syn::{AttrStyle, Expr, ItemFn, Stmt}; - fn expand( - args: proc_macro2::TokenStream, - item: proc_macro2::TokenStream, - ) -> syn::Result { - super::expand(args, item, &syn::parse_quote!(::fearless_simd)) - } + use super::expand; fn expand_ok(item: proc_macro2::TokenStream) -> proc_macro2::TokenStream { expand(proc_macro2::TokenStream::new(), item).expect("macro expansion should succeed") @@ -295,10 +276,24 @@ mod tests { sum } }); + // Safe user code must not acquire an unsafe call to a caller-resolved + // name. Only the library helper may introduce dispatcher unsafety. + assert!(!expanded.to_string().contains("unsafe")); let parsed: ItemFn = syn::parse2(expanded).expect("expanded function parses"); let Some(Stmt::Expr(Expr::MethodCall(call), None)) = parsed.block.stmts.last() else { panic!("function tail should be a method call"); }; + let Expr::Paren(receiver) = &*call.receiver else { + panic!("dispatcher receiver should be parenthesized"); + }; + let Expr::Macro(dispatcher) = &*receiver.expr else { + panic!("dispatcher receiver should be a library macro invocation"); + }; + assert!(dispatcher.mac.path.leading_colon.is_none()); + assert_eq!( + dispatcher.mac.path.to_token_stream().to_string(), + "fearless_simd :: __fearless_simd_dispatch" + ); let Some(Expr::Closure(closure)) = call.args.last() else { panic!("last dispatcher argument should be a closure"); }; @@ -395,10 +390,12 @@ mod tests { let inner_attr = text .find("# ! [allow") .expect("inner attribute is retained"); - let call = text.find("__FearlessDispatch").expect("dispatcher exists"); + let call = text + .find("__fearless_simd_dispatch") + .expect("dispatcher invocation exists"); assert!(inner_attr < call); assert_eq!(text.matches("inline (never)").count(), 1); - assert_eq!(text.matches("inline (always)").count(), 2); + assert_eq!(text.matches("inline (always)").count(), 1); assert!(text.contains("target_feature")); } From 173bbc1f5aefab8f9b4e150fbc8d4fe1264d9401 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 10 Sep 2026 15:09:45 +0100 Subject: [PATCH 17/20] Clean up and simplify the code a bit --- fearless_simd/src/kernel_macros.rs | 99 ++++++++++++------------------ fearless_simd_macros/Cargo.toml | 2 +- fearless_simd_macros/src/lib.rs | 3 +- 3 files changed, 40 insertions(+), 64 deletions(-) diff --git a/fearless_simd/src/kernel_macros.rs b/fearless_simd/src/kernel_macros.rs index 1061324c..608476b9 100644 --- a/fearless_simd/src/kernel_macros.rs +++ b/fearless_simd/src/kernel_macros.rs @@ -145,7 +145,7 @@ macro_rules! __fearless_simd_kernel_cfg { }; } -// Internal interface for `kernel!` and `__fearless_simd_dispatch!`, which backs +// Internal interface for `kernel!` and `__fearless_simd_dispatch_entry!`, which backs // the argument-taking `#[simd]` expansion in `fearless_simd_macros`. #[doc(hidden)] #[macro_export] @@ -215,83 +215,33 @@ macro_rules! __fearless_simd_dispatch { match $crate::Simd::level(simd) { #[cfg(target_arch = "aarch64")] $crate::Level::Neon(proof) => { - $crate::__fearless_simd_kernel_target_fn! { - Neon, - #[inline] - fn entry<$($argument_type,)* F, R>( - _: $crate::Neon, $($argument: $argument_type,)* f: F, - ) -> R - where F: ::core::ops::FnOnce($($argument_type),*) -> R { - f($($argument),*) - } + $crate::__fearless_simd_dispatch_entry! { + Neon, proof, f; $($argument_type => $argument),* } - // SAFETY: the genuine proof token establishes all the - // target features attached by the library's helper. - unsafe { entry(proof, $($argument,)* f) } } #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] $crate::Level::Sse2(proof) => { - $crate::__fearless_simd_kernel_target_fn! { - Sse2, - #[inline] - fn entry<$($argument_type,)* F, R>( - _: $crate::Sse2, $($argument: $argument_type,)* f: F, - ) -> R - where F: ::core::ops::FnOnce($($argument_type),*) -> R { - f($($argument),*) - } + $crate::__fearless_simd_dispatch_entry! { + Sse2, proof, f; $($argument_type => $argument),* } - // SAFETY: the genuine proof token establishes all the - // target features attached by the library's helper. - unsafe { entry(proof, $($argument,)* f) } } #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] $crate::Level::Sse4_2(proof) => { - $crate::__fearless_simd_kernel_target_fn! { - Sse4_2, - #[inline] - fn entry<$($argument_type,)* F, R>( - _: $crate::Sse4_2, $($argument: $argument_type,)* f: F, - ) -> R - where F: ::core::ops::FnOnce($($argument_type),*) -> R { - f($($argument),*) - } + $crate::__fearless_simd_dispatch_entry! { + Sse4_2, proof, f; $($argument_type => $argument),* } - // SAFETY: the genuine proof token establishes all the - // target features attached by the library's helper. - unsafe { entry(proof, $($argument,)* f) } } #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] $crate::Level::Avx2(proof) => { - $crate::__fearless_simd_kernel_target_fn! { - Avx2, - #[inline] - fn entry<$($argument_type,)* F, R>( - _: $crate::Avx2, $($argument: $argument_type,)* f: F, - ) -> R - where F: ::core::ops::FnOnce($($argument_type),*) -> R { - f($($argument),*) - } + $crate::__fearless_simd_dispatch_entry! { + Avx2, proof, f; $($argument_type => $argument),* } - // SAFETY: the genuine proof token establishes all the - // target features attached by the library's helper. - unsafe { entry(proof, $($argument,)* f) } } #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] $crate::Level::Avx512(proof) => { - $crate::__fearless_simd_kernel_target_fn! { - Avx512, - #[inline] - fn entry<$($argument_type,)* F, R>( - _: $crate::Avx512, $($argument: $argument_type,)* f: F, - ) -> R - where F: ::core::ops::FnOnce($($argument_type),*) -> R { - f($($argument),*) - } + $crate::__fearless_simd_dispatch_entry! { + Avx512, proof, f; $($argument_type => $argument),* } - // SAFETY: the genuine proof token establishes all the - // target features attached by the library's helper. - unsafe { entry(proof, $($argument,)* f) } } #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))] $crate::Level::WasmSimd128(_) => f($($argument),*), @@ -312,6 +262,33 @@ macro_rules! __fearless_simd_dispatch { }}; } +// Generate and call one backend entry for `__fearless_simd_dispatch!`. +// Keep even direct invocations safe: the selected level determines both +// the genuine proof type and its target features. Bind inputs outside the +// unsafe block: even an identifier could name a mutable or extern static. +#[doc(hidden)] +#[macro_export] +macro_rules! __fearless_simd_dispatch_entry { + ($level:ident, $proof:ident, $f:ident; $($argument_type:ident => $argument:ident),*) => {{ + $crate::__fearless_simd_kernel_target_fn! { + $level, + #[inline] + fn entry<$($argument_type,)* F, R>( + _: $crate::$level, $($argument: $argument_type,)* f: F, + ) -> R + where F: ::core::ops::FnOnce($($argument_type),*) -> R { + f($($argument),*) + } + } + let proof = $proof; + $(let $argument = $argument;)* + let f = $f; + // SAFETY: the genuine proof token establishes all the target features + // attached by the library's helper for this same level. + unsafe { entry(proof, $($argument,)* f) } + }}; +} + /// The implementation protocol must not accept caller-supplied token types or /// target-feature annotations. /// diff --git a/fearless_simd_macros/Cargo.toml b/fearless_simd_macros/Cargo.toml index be34c687..8139019a 100644 --- a/fearless_simd_macros/Cargo.toml +++ b/fearless_simd_macros/Cargo.toml @@ -17,7 +17,7 @@ proc-macro = true [dependencies] proc-macro2 = { workspace = true } quote = { workspace = true } -syn = { workspace = true, features = ["fold"] } +syn = { workspace = true, features = ["clone-impls", "fold"] } [lints] workspace = true diff --git a/fearless_simd_macros/src/lib.rs b/fearless_simd_macros/src/lib.rs index 7d6eee31..13112dde 100644 --- a/fearless_simd_macros/src/lib.rs +++ b/fearless_simd_macros/src/lib.rs @@ -48,8 +48,7 @@ fn expand(args: TokenStream2, item: TokenStream2) -> Result { // Give the closure the same expected return type so branch and early-return // coercions happen inside its body. Closures cannot name `impl Trait`, so // infer those parts while preserving the surrounding type structure. - let output = &function.sig.output; - let closure_output = InferImplTrait.fold_return_type(syn::parse_quote!(#output)); + let closure_output = InferImplTrait.fold_return_type(function.sig.output.clone()); // Wrapping the body in simd.vectorize(|| ...) puts its captured arguments // into a closure struct. If the target-feature helper remains out of line, From 307c22bd4b80fe79926d77ddb06b0f734d23cf88 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 10 Sep 2026 15:15:22 +0100 Subject: [PATCH 18/20] Add forgotten tests --- .../tests/ui/simd/fail/counterfeit_library.rs | 47 +++++++++++++++ .../ui/simd/fail/counterfeit_library.stderr | 20 +++++++ .../tests/ui/simd/fail/entry_static.rs | 17 ++++++ .../tests/ui/simd/fail/entry_static.stderr | 11 ++++ .../tests/ui/simd/pass/argument_hygiene.rs | 57 +++++++++++++++++++ .../tests/ui/simd/renamed/.gitignore | 1 + .../tests/ui/simd/renamed/Cargo.toml | 11 ++++ .../tests/ui/simd/renamed/src/lib.rs | 37 ++++++++++++ 8 files changed, 201 insertions(+) create mode 100644 fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/fail/entry_static.rs create mode 100644 fearless_simd_tests/tests/ui/simd/fail/entry_static.stderr create mode 100644 fearless_simd_tests/tests/ui/simd/pass/argument_hygiene.rs create mode 100644 fearless_simd_tests/tests/ui/simd/renamed/.gitignore create mode 100644 fearless_simd_tests/tests/ui/simd/renamed/Cargo.toml create mode 100644 fearless_simd_tests/tests/ui/simd/renamed/src/lib.rs diff --git a/fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.rs b/fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.rs new file mode 100644 index 00000000..aadf7756 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.rs @@ -0,0 +1,47 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +#![forbid(unsafe_code)] + +use fearless_simd_macros::simd; + +// Re-exporting the genuine helper must not let a counterfeit namespace supply +// its own proof that a target feature is available. +#[allow(dead_code)] +mod fearless_simd { + pub use ::fearless_simd::__fearless_simd_dispatch; + + #[derive(Clone, Copy)] + pub struct CounterfeitToken; + + pub enum Level { + Avx512(CounterfeitToken), + } + + pub trait Simd { + fn level(self) -> Level; + + fn vectorize R, R>(self, f: F) -> R + where + Self: Sized, + { + f() + } + } + + impl Simd for CounterfeitToken { + fn level(self) -> Level { + Level::Avx512(self) + } + } +} + +#[simd] +fn counterfeit(simd: S, value: u32) -> u32 { + let _ = simd.level(); + value +} + +fn main() { + let _ = counterfeit(fearless_simd::CounterfeitToken, 1); +} diff --git a/fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.stderr b/fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.stderr new file mode 100644 index 00000000..d0f96e62 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.stderr @@ -0,0 +1,20 @@ +error[E0277]: the trait bound `S: fearless_simd::Simd` is not satisfied + --> tests/ui/simd/fail/counterfeit_library.rs:39:1 + | +39 | #[simd] + | ^^^^^^^ + | | + | the trait `fearless_simd::Simd` is not implemented for `S` + | required by a bound introduced by this call + | + = note: `S` implements similarly named trait `fearless_simd::Simd`, but not `fearless_simd::Simd` +note: required by a bound in `__FearlessDispatch::call` + --> tests/ui/simd/fail/counterfeit_library.rs:39:1 + | +39 | #[simd] + | ^^^^^^^ required by this bound in `__FearlessDispatch::call` + = note: this error originates in the attribute macro `simd` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider further restricting type parameter `S` with trait `Simd` + | +40 | fn counterfeit(simd: S, value: u32) -> u32 { + | +++++++++++++++++++++ diff --git a/fearless_simd_tests/tests/ui/simd/fail/entry_static.rs b/fearless_simd_tests/tests/ui/simd/fail/entry_static.rs new file mode 100644 index 00000000..a26a1ea2 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/entry_static.rs @@ -0,0 +1,17 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +#![allow(dead_code)] + +static mut CALLBACK: fn(u32) -> u32 = |value| value; + +// Identifiers supplied directly to the helper must be evaluated outside its +// unsafe block, so they cannot hide an unsafe read of a mutable static. +fn forward(proof: fearless_simd::Sse2, value: u32) -> u32 { + fearless_simd::__fearless_simd_dispatch_entry! { + Sse2, proof, CALLBACK; + A => value + } +} + +fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/entry_static.stderr b/fearless_simd_tests/tests/ui/simd/fail/entry_static.stderr new file mode 100644 index 00000000..92532429 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/fail/entry_static.stderr @@ -0,0 +1,11 @@ +error[E0133]: use of mutable static is unsafe and requires unsafe block + --> tests/ui/simd/fail/entry_static.rs:11:5 + | +11 | / fearless_simd::__fearless_simd_dispatch_entry! { +12 | | Sse2, proof, CALLBACK; +13 | | A => value +14 | | } + | |_____^ use of mutable static + | + = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior + = note: this error originates in the macro `fearless_simd::__fearless_simd_dispatch_entry` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/fearless_simd_tests/tests/ui/simd/pass/argument_hygiene.rs b/fearless_simd_tests/tests/ui/simd/pass/argument_hygiene.rs new file mode 100644 index 00000000..7d3a233e --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/pass/argument_hygiene.rs @@ -0,0 +1,57 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +#![deny(warnings)] +#![forbid(unsafe_code)] + +use fearless_simd::{Fallback, Simd}; +use fearless_simd_macros::simd; + +struct __FearlessDispatch(u32); + +fn entry(value: u32) -> u32 { + value + 1 +} + +#[simd] +fn unused_token(simd: S, value: u32) -> u32 { + value +} + +#[simd] +fn names( + _: S, + __fearless_argument_0: u32, + __fearless_argument_2: u32, + r#type: __FearlessDispatch, + #[expect(unused_variables)] unused: u32, +) -> u32 { + entry(__fearless_argument_0 + __fearless_argument_2 + r#type.0) +} + +macro_rules! pair { + ($a:ident, $b:ident) => { ($a, $b) }; +} + +#[simd] +fn pattern(_: S, pair!(a, b): (u32, u32)) -> u32 { + a + b +} + +impl __FearlessDispatch { + #[simd] + fn method(&self, _: S, value: u32) -> u32 { + macro_rules! receiver { + () => { self.0 }; + } + receiver!() + value + } +} + +fn main() { + let simd = Fallback::new(); + assert_eq!(unused_token(simd, 42), 42); + assert_eq!(names(simd, 1, 2, __FearlessDispatch(3), 0), 7); + assert_eq!(pattern(simd, (1, 2)), 3); + assert_eq!(__FearlessDispatch(3).method(simd, 4), 7); +} diff --git a/fearless_simd_tests/tests/ui/simd/renamed/.gitignore b/fearless_simd_tests/tests/ui/simd/renamed/.gitignore new file mode 100644 index 00000000..5a44eef0 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/renamed/.gitignore @@ -0,0 +1 @@ +/Cargo.lock diff --git a/fearless_simd_tests/tests/ui/simd/renamed/Cargo.toml b/fearless_simd_tests/tests/ui/simd/renamed/Cargo.toml new file mode 100644 index 00000000..e5878a1e --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/renamed/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "simd-renamed-dependency" +version = "0.0.0" +edition = "2024" +publish = false + +[workspace] + +[dependencies] +simd_backend = { package = "fearless_simd", path = "../../../../../fearless_simd", default-features = false, features = ["libm"] } +fearless_simd_macros = { path = "../../../../../fearless_simd_macros" } diff --git a/fearless_simd_tests/tests/ui/simd/renamed/src/lib.rs b/fearless_simd_tests/tests/ui/simd/renamed/src/lib.rs new file mode 100644 index 00000000..d1e17ff8 --- /dev/null +++ b/fearless_simd_tests/tests/ui/simd/renamed/src/lib.rs @@ -0,0 +1,37 @@ +// Copyright 2026 the Fearless_SIMD Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +#![no_std] +#![forbid(unsafe_code)] + +use fearless_simd_macros::simd; +use simd_backend as fearless_simd; + +#[simd] +pub fn sum(_: S, a: u64, b: u64, c: u64) -> u64 { + a.wrapping_add(b).wrapping_add(c) +} + +// Instantiate the macro without force_support_fallback and without std. This +// also checks that it does not need Simd imported to resolve generated calls. +pub fn baseline_sum() -> u64 { + simd_backend::dispatch!(simd_backend::Level::baseline(), simd => sum(simd, 1, 2, 3)) +} + +pub mod facade { + pub use simd_backend; +} + +pub mod through_reexport { + use crate::facade::simd_backend as fearless_simd; + use fearless_simd_macros::simd; + + #[simd] + pub fn sum(_: S, a: u64, b: u64, c: u64) -> u64 { + a.wrapping_add(b).wrapping_add(c) + } + + pub fn baseline_sum() -> u64 { + fearless_simd::dispatch!(fearless_simd::Level::baseline(), simd => sum(simd, 1, 2, 3)) + } +} From ea5b81c636e3d0fd42ea3fa26104477dfcafaba0 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 10 Sep 2026 16:33:59 +0100 Subject: [PATCH 19/20] port from trybuild to ui_test to avoid depending on exact compiler output that changes between compiler versions --- Cargo.lock | 628 ++++++++++++++++-- fearless_simd_tests/Cargo.toml | 2 +- fearless_simd_tests/README.md | 35 + fearless_simd_tests/tests/simd_ui.rs | 58 +- .../tests/ui/simd/fail/arguments.rs | 1 + .../tests/ui/simd/fail/arguments.stderr | 5 - .../tests/ui/simd/fail/async.rs | 1 + .../tests/ui/simd/fail/async.stderr | 5 - .../tests/ui/simd/fail/at_token.rs | 1 + .../tests/ui/simd/fail/at_token.stderr | 5 - .../tests/ui/simd/fail/bodyless.rs | 1 + .../tests/ui/simd/fail/bodyless.stderr | 5 - .../tests/ui/simd/fail/const.rs | 1 + .../tests/ui/simd/fail/const.stderr | 5 - .../tests/ui/simd/fail/counterfeit_library.rs | 1 + .../ui/simd/fail/counterfeit_library.stderr | 20 - .../tests/ui/simd/fail/default_modifier.rs | 1 + .../ui/simd/fail/default_modifier.stderr | 5 - .../tests/ui/simd/fail/destructured_token.rs | 1 + .../ui/simd/fail/destructured_token.stderr | 5 - .../tests/ui/simd/fail/entry_static.rs | 1 + .../tests/ui/simd/fail/entry_static.stderr | 11 - .../tests/ui/simd/fail/instruction_set.rs | 1 + .../tests/ui/simd/fail/instruction_set.stderr | 5 - .../tests/ui/simd/fail/naked.rs | 1 + .../tests/ui/simd/fail/naked.stderr | 5 - .../tests/ui/simd/fail/no_token.rs | 1 + .../tests/ui/simd/fail/no_token.stderr | 7 - .../tests/ui/simd/fail/non_function.rs | 1 + .../tests/ui/simd/fail/non_function.stderr | 5 - .../tests/ui/simd/fail/parameter_cfg.rs | 1 + .../tests/ui/simd/fail/parameter_cfg.stderr | 5 - .../tests/ui/simd/fail/ref_token.rs | 1 + .../tests/ui/simd/fail/ref_token.stderr | 5 - .../tests/ui/simd/fail/track_caller.rs | 1 + .../tests/ui/simd/fail/track_caller.stderr | 5 - .../tests/ui/simd/fail/variadic.rs | 1 + .../tests/ui/simd/fail/variadic.stderr | 5 - .../tests/ui/simd/pass/argument_hygiene.rs | 2 + .../tests/ui/simd/pass/functions.rs | 2 + .../tests/ui/simd/pass/methods.rs | 2 + .../tests/ui/simd/pass/return_types.rs | 2 + 42 files changed, 677 insertions(+), 179 deletions(-) delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/arguments.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/async.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/at_token.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/bodyless.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/const.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/default_modifier.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/destructured_token.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/entry_static.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/instruction_set.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/naked.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/no_token.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/non_function.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/ref_token.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/track_caller.stderr delete mode 100644 fearless_simd_tests/tests/ui/simd/fail/variadic.stderr diff --git a/Cargo.lock b/Cargo.lock index 54019df2..93bd9daf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,31 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "annotate-snippets" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" +dependencies = [ + "anstyle", + "unicode-width", +] + [[package]] name = "anstream" version = "0.6.19" @@ -38,7 +63,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -49,7 +74,7 @@ checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -58,6 +83,82 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122ec45a44b270afd1402f351b782c676b173e3c3fb28d86ff7ebfb4d86a4ee4" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "clap" version = "4.5.40" @@ -98,6 +199,33 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -105,10 +233,63 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] -name = "equivalent" -version = "1.0.2" +name = "colored" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "comma" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335" + +[[package]] +name = "console" +version = "0.16.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "eyre" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08309dbcc659c5549a24ddb9b27027640641b282ef5768267c7e675558986a3" +dependencies = [ + "autocfg", + "indenter", + "once_cell", +] [[package]] name = "fastrand" @@ -159,20 +340,38 @@ dependencies = [ "fearless_simd", "fearless_simd_dev_macros", "fearless_simd_macros", - "trybuild", + "ui_test", ] [[package]] -name = "glob" -version = "0.3.4" +name = "futures-core" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] -name = "hashbrown" -version = "0.17.1" +name = "futures-task" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "heck" @@ -181,13 +380,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "indexmap" -version = "2.14.0" +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indicatif" +version = "0.18.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ - "equivalent", - "hashbrown", + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", ] [[package]] @@ -202,6 +410,35 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "levenshtein" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + [[package]] name = "libm" version = "0.2.15" @@ -214,12 +451,63 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + [[package]] name = "once_cell_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "owo-colors" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "prettydiff" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac17546d82912e64874e3d5b40681ce32eac4e5834344f51efcf689ff1550a65" +dependencies = [ + "owo-colors", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -238,6 +526,74 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustfix" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fa69b198d894d84e23afde8e9ab2af4400b2cba20d6bf2b428a8b01c222c5a" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde" version = "1.0.229" @@ -245,6 +601,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -281,12 +638,29 @@ dependencies = [ ] [[package]] -name = "serde_spanned" -version = "1.1.1" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "serde_core", + "lazy_static", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "spanned" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4b0c055fde758f086eb4a6e73410247df8a3837fd606d2caeeaf72aa566d" +dependencies = [ + "anyhow", + "bstr", + "color-eyre", ] [[package]] @@ -318,72 +692,119 @@ dependencies = [ ] [[package]] -name = "target-triple" -version = "1.0.1" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] [[package]] -name = "termcolor" -version = "1.4.1" +name = "thiserror" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "winapi-util", + "thiserror-impl 2.0.20", ] [[package]] -name = "toml" -version = "1.1.4+spec-1.1.0" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", + "proc-macro2", + "quote", + "syn 2.0.103", ] [[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +name = "thiserror-impl" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ - "serde_core", + "proc-macro2", + "quote", + "syn 3.0.4", ] [[package]] -name = "toml_parser" -version = "1.1.3+spec-1.1.0" +name = "thread_local" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ - "winnow", + "cfg-if", ] [[package]] -name = "toml_writer" -version = "1.1.2+spec-1.1.0" +name = "tracing" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] [[package]] -name = "trybuild" -version = "1.0.120" +name = "ui_test" +version = "0.30.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4" +checksum = "8c8811281d587a786747c0c49245925016c07767bc996305bdd34d5ce076786a" dependencies = [ - "glob", + "annotate-snippets", + "anyhow", + "bstr", + "cargo-platform", + "cargo_metadata", + "color-eyre", + "colored", + "comma", + "crossbeam-channel", + "indicatif", + "levenshtein", + "prettydiff", + "regex", + "rustc_version", + "rustfix", "serde", - "serde_derive", "serde_json", - "target-triple", - "termcolor", - "toml", + "spanned", ] [[package]] @@ -392,6 +813,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "utf8parse" version = "0.2.2" @@ -399,14 +832,72 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "winapi-util" -version = "0.1.11" +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.4", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "windows-sys", + "js-sys", + "wasm-bindgen", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.59.0" @@ -416,6 +907,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -480,12 +980,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" - [[package]] name = "zmij" version = "1.0.23" diff --git a/fearless_simd_tests/Cargo.toml b/fearless_simd_tests/Cargo.toml index c496ef65..7237c903 100644 --- a/fearless_simd_tests/Cargo.toml +++ b/fearless_simd_tests/Cargo.toml @@ -37,4 +37,4 @@ fearless_simd = { workspace = true, features = [ fearless_simd_dev_macros = { workspace = true } [target.'cfg(all(target_arch = "x86_64", target_os = "linux"))'.dev-dependencies] -trybuild = "1.0.120" +ui_test = "0.30.7" diff --git a/fearless_simd_tests/README.md b/fearless_simd_tests/README.md index 2a1a44f3..b21252c6 100644 --- a/fearless_simd_tests/README.md +++ b/fearless_simd_tests/README.md @@ -6,6 +6,41 @@ This is a development-only crate for testing `fearless_simd`. +### Testing the SIMD attribute + +The UI suite runs explicitly on native x86-64 Linux, including on the minimum +supported Rust version: + +```sh +cargo test -p fearless_simd_tests --locked --test simd_ui --target x86_64-unknown-linux-gnu -- --ignored +cargo +1.89 test -p fearless_simd_tests --locked --test simd_ui --target x86_64-unknown-linux-gnu -- --ignored +``` + +Fixtures in `tests/ui/simd/fail` use `ui_test` annotations to check an error at +the annotated source line. For diagnostics produced by the attribute, match a +focused part of the primary message: + +```rust,ignore +#[simd(token = simd)] //~ ERROR: `#[simd]` does not accept arguments +``` + +For compiler diagnostics, match the error code, with a comment explaining what +the fixture protects: + +```rust,ignore +#[simd] //~ E0277 +``` + +An annotation matches either a message or a code, not both. Missing expected +errors and unexpected additional errors fail the test. Compiler notes, help, +warnings, and rendered output are not compared, and rustfix checks are disabled. +There are no `.stderr` snapshots to update. Use `//~^` to refer to the preceding +line when an inline annotation is inconvenient. + +Fixtures in `tests/ui/simd/pass` use `//@run` so their runtime assertions execute +as well as compiling. The separate renamed-dependency fixture checks a renamed +import and a library re-export without the standard library or forced fallback. + ### Testing WebAssembly +simd128 diff --git a/fearless_simd_tests/tests/simd_ui.rs b/fearless_simd_tests/tests/simd_ui.rs index 5509f13d..89b4a268 100644 --- a/fearless_simd_tests/tests/simd_ui.rs +++ b/fearless_simd_tests/tests/simd_ui.rs @@ -7,11 +7,63 @@ #[test] #[ignore = "run explicitly in the native Linux CI job"] fn simd_ui() { - let tests = trybuild::TestCases::new(); - tests.pass("tests/ui/simd/pass/*.rs"); - tests.compile_fail("tests/ui/simd/fail/*.rs"); + use ui_test::custom_flags::{edition::Edition, rustfix::RustfixMode}; + use ui_test::dependencies::DependencyBuilder; let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + let mut config = ui_test::Config::rustc(manifest_dir.join("tests/ui/simd")); + config.out_dir = manifest_dir.join("../target/tests/simd-ui"); + // Match annotated errors and their locations, not rustc's rendered notes, + // suggestions, or formatting. Unexpected errors still fail the test. + config.output_conflict_handling = ui_test::ignore_output_conflict; + config + .comment_defaults + .base() + .set_custom("edition", Edition("2024".into())); + config + .comment_defaults + .base() + .set_custom("rustfix", RustfixMode::Disabled); + + let mut dependencies = DependencyBuilder { + crate_manifest_path: manifest_dir.join("Cargo.toml"), + ..DependencyBuilder::default() + }; + dependencies.program.program = env!("CARGO").into(); + // DependencyBuilder builds ordinary dependencies, so explicitly request + // the same features our fixtures otherwise obtain from dev-dependencies. + dependencies.program.args.extend( + [ + "--lib", + "--locked", + "--offline", + "--features=fearless_simd/std,fearless_simd/force_support_fallback", + ] + .map(Into::into), + ); + config + .comment_defaults + .base() + .set_custom("dependencies", dependencies); + + let pass_dir = config.root_dir.join("pass"); + let fail_dir = config.root_dir.join("fail"); + // libtest already consumed --ignored. run_tests() would parse it again + // and skip our ordinary UI cases; use the entry point without CLI parsing. + ui_test::run_tests_generic( + vec![config], + move |path, config| { + if path.starts_with(&pass_dir) || path.starts_with(&fail_dir) { + ui_test::default_file_filter(path, config) + } else { + None + } + }, + ui_test::default_per_file_config, + ui_test::status_emitter::Text::verbose(), + ) + .expect("SIMD UI fixtures should pass"); + let status = std::process::Command::new(env!("CARGO")) .args(["check", "--offline", "--manifest-path"]) .arg(manifest_dir.join("tests/ui/simd/renamed/Cargo.toml")) diff --git a/fearless_simd_tests/tests/ui/simd/fail/arguments.rs b/fearless_simd_tests/tests/ui/simd/fail/arguments.rs index ddd50d91..7a30cdca 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/arguments.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/arguments.rs @@ -4,6 +4,7 @@ use fearless_simd_macros::simd; #[simd(token = simd)] +//~^ ERROR: `#[simd]` does not accept arguments fn arguments_are_not_supported(simd: S) { let _ = simd; } diff --git a/fearless_simd_tests/tests/ui/simd/fail/arguments.stderr b/fearless_simd_tests/tests/ui/simd/fail/arguments.stderr deleted file mode 100644 index d137a0ab..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/arguments.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: `#[simd]` does not accept arguments - --> tests/ui/simd/fail/arguments.rs:6:8 - | -6 | #[simd(token = simd)] - | ^^^^^^^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/async.rs b/fearless_simd_tests/tests/ui/simd/fail/async.rs index cbde7bf0..b1bb5073 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/async.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/async.rs @@ -5,6 +5,7 @@ use fearless_simd_macros::simd; #[simd] async fn async_is_not_supported(simd: S) { + //~^ ERROR: `#[simd]` does not support async functions let _ = simd; } diff --git a/fearless_simd_tests/tests/ui/simd/fail/async.stderr b/fearless_simd_tests/tests/ui/simd/fail/async.stderr deleted file mode 100644 index 17cdda2b..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/async.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: `#[simd]` does not support async functions - --> tests/ui/simd/fail/async.rs:7:1 - | -7 | async fn async_is_not_supported(simd: S) { - | ^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/at_token.rs b/fearless_simd_tests/tests/ui/simd/fail/at_token.rs index fe49fa1b..d5a2facf 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/at_token.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/at_token.rs @@ -5,6 +5,7 @@ use fearless_simd_macros::simd; #[simd] fn at_token_is_not_supported(simd @ _: S) { + //~^ ERROR: the SIMD token parameter cannot use an `@` subpattern let _ = simd; } diff --git a/fearless_simd_tests/tests/ui/simd/fail/at_token.stderr b/fearless_simd_tests/tests/ui/simd/fail/at_token.stderr deleted file mode 100644 index a4c19dd7..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/at_token.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: the SIMD token parameter cannot use an `@` subpattern - --> tests/ui/simd/fail/at_token.rs:7:59 - | -7 | fn at_token_is_not_supported(simd @ _: S) { - | ^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/bodyless.rs b/fearless_simd_tests/tests/ui/simd/fail/bodyless.rs index 2ae8b7f4..69191eb1 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/bodyless.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/bodyless.rs @@ -6,6 +6,7 @@ use fearless_simd_macros::simd; trait Operation { #[simd] fn bodyless(simd: S); + //~^ ERROR: expected curly braces } fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/bodyless.stderr b/fearless_simd_tests/tests/ui/simd/fail/bodyless.stderr deleted file mode 100644 index 3852d821..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/bodyless.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: expected curly braces - --> tests/ui/simd/fail/bodyless.rs:8:49 - | -8 | fn bodyless(simd: S); - | ^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/const.rs b/fearless_simd_tests/tests/ui/simd/fail/const.rs index 228233c4..a6c003fe 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/const.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/const.rs @@ -5,6 +5,7 @@ use fearless_simd_macros::simd; #[simd] const fn const_is_not_supported(simd: S) { + //~^ ERROR: `#[simd]` does not support const functions let _ = simd; } diff --git a/fearless_simd_tests/tests/ui/simd/fail/const.stderr b/fearless_simd_tests/tests/ui/simd/fail/const.stderr deleted file mode 100644 index b4bd65fe..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/const.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: `#[simd]` does not support const functions - --> tests/ui/simd/fail/const.rs:7:1 - | -7 | const fn const_is_not_supported(simd: S) { - | ^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.rs b/fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.rs index aadf7756..b2b88fa8 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.rs @@ -37,6 +37,7 @@ mod fearless_simd { } #[simd] +//~^ E0277 fn counterfeit(simd: S, value: u32) -> u32 { let _ = simd.level(); value diff --git a/fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.stderr b/fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.stderr deleted file mode 100644 index d0f96e62..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/counterfeit_library.stderr +++ /dev/null @@ -1,20 +0,0 @@ -error[E0277]: the trait bound `S: fearless_simd::Simd` is not satisfied - --> tests/ui/simd/fail/counterfeit_library.rs:39:1 - | -39 | #[simd] - | ^^^^^^^ - | | - | the trait `fearless_simd::Simd` is not implemented for `S` - | required by a bound introduced by this call - | - = note: `S` implements similarly named trait `fearless_simd::Simd`, but not `fearless_simd::Simd` -note: required by a bound in `__FearlessDispatch::call` - --> tests/ui/simd/fail/counterfeit_library.rs:39:1 - | -39 | #[simd] - | ^^^^^^^ required by this bound in `__FearlessDispatch::call` - = note: this error originates in the attribute macro `simd` (in Nightly builds, run with -Z macro-backtrace for more info) -help: consider further restricting type parameter `S` with trait `Simd` - | -40 | fn counterfeit(simd: S, value: u32) -> u32 { - | +++++++++++++++++++++ diff --git a/fearless_simd_tests/tests/ui/simd/fail/default_modifier.rs b/fearless_simd_tests/tests/ui/simd/fail/default_modifier.rs index a21b5f90..4e8283bd 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/default_modifier.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/default_modifier.rs @@ -14,6 +14,7 @@ trait Operation { impl Operation for Fixture { #[simd] default fn operation(simd: S) { + //~^ ERROR: expected `fn` let _ = simd; } } diff --git a/fearless_simd_tests/tests/ui/simd/fail/default_modifier.stderr b/fearless_simd_tests/tests/ui/simd/fail/default_modifier.stderr deleted file mode 100644 index 978a49f2..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/default_modifier.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: expected `fn` - --> tests/ui/simd/fail/default_modifier.rs:16:5 - | -16 | default fn operation(simd: S) { - | ^^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/destructured_token.rs b/fearless_simd_tests/tests/ui/simd/fail/destructured_token.rs index 480f1e90..161139f8 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/destructured_token.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/destructured_token.rs @@ -5,6 +5,7 @@ use fearless_simd_macros::simd; #[simd] fn destructured_token_is_not_supported((simd,): (S,)) { + //~^ ERROR: the SIMD token parameter must be an identifier or `_` let _ = simd; } diff --git a/fearless_simd_tests/tests/ui/simd/fail/destructured_token.stderr b/fearless_simd_tests/tests/ui/simd/fail/destructured_token.stderr deleted file mode 100644 index e82ccef1..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/destructured_token.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: the SIMD token parameter must be an identifier or `_` - --> tests/ui/simd/fail/destructured_token.rs:7:64 - | -7 | fn destructured_token_is_not_supported((simd,): (S,)) { - | ^^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/entry_static.rs b/fearless_simd_tests/tests/ui/simd/fail/entry_static.rs index a26a1ea2..5b2fd3ce 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/entry_static.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/entry_static.rs @@ -9,6 +9,7 @@ static mut CALLBACK: fn(u32) -> u32 = |value| value; // unsafe block, so they cannot hide an unsafe read of a mutable static. fn forward(proof: fearless_simd::Sse2, value: u32) -> u32 { fearless_simd::__fearless_simd_dispatch_entry! { + //~^ E0133 Sse2, proof, CALLBACK; A => value } diff --git a/fearless_simd_tests/tests/ui/simd/fail/entry_static.stderr b/fearless_simd_tests/tests/ui/simd/fail/entry_static.stderr deleted file mode 100644 index 92532429..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/entry_static.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error[E0133]: use of mutable static is unsafe and requires unsafe block - --> tests/ui/simd/fail/entry_static.rs:11:5 - | -11 | / fearless_simd::__fearless_simd_dispatch_entry! { -12 | | Sse2, proof, CALLBACK; -13 | | A => value -14 | | } - | |_____^ use of mutable static - | - = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior - = note: this error originates in the macro `fearless_simd::__fearless_simd_dispatch_entry` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/fearless_simd_tests/tests/ui/simd/fail/instruction_set.rs b/fearless_simd_tests/tests/ui/simd/fail/instruction_set.rs index 7a0e2653..5f2ac9e7 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/instruction_set.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/instruction_set.rs @@ -5,6 +5,7 @@ use fearless_simd_macros::simd; #[simd] #[instruction_set(arm::a32)] +//~^ ERROR: `#[simd]` cannot be combined with `#[instruction_set]` fn instruction_set_is_not_supported(simd: S) { let _ = simd; } diff --git a/fearless_simd_tests/tests/ui/simd/fail/instruction_set.stderr b/fearless_simd_tests/tests/ui/simd/fail/instruction_set.stderr deleted file mode 100644 index c09066da..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/instruction_set.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: `#[simd]` cannot be combined with `#[instruction_set]` - --> tests/ui/simd/fail/instruction_set.rs:7:1 - | -7 | #[instruction_set(arm::a32)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/naked.rs b/fearless_simd_tests/tests/ui/simd/fail/naked.rs index 616a3967..34113985 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/naked.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/naked.rs @@ -5,6 +5,7 @@ use fearless_simd_macros::simd; #[simd] #[naked] +//~^ ERROR: `#[simd]` cannot be used on a naked function fn naked_is_not_supported(simd: S) { let _ = simd; } diff --git a/fearless_simd_tests/tests/ui/simd/fail/naked.stderr b/fearless_simd_tests/tests/ui/simd/fail/naked.stderr deleted file mode 100644 index ff9ad10b..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/naked.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: `#[simd]` cannot be used on a naked function - --> tests/ui/simd/fail/naked.rs:7:1 - | -7 | #[naked] - | ^^^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/no_token.rs b/fearless_simd_tests/tests/ui/simd/fail/no_token.rs index 3a355ded..2c606341 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/no_token.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/no_token.rs @@ -4,6 +4,7 @@ use fearless_simd_macros::simd; #[simd] +//~^ ERROR: `#[simd]` requires a SIMD token parameter after any receiver fn token_is_required() {} fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/no_token.stderr b/fearless_simd_tests/tests/ui/simd/fail/no_token.stderr deleted file mode 100644 index dcc3d7dc..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/no_token.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error: `#[simd]` requires a SIMD token parameter after any receiver - --> tests/ui/simd/fail/no_token.rs:6:1 - | -6 | #[simd] - | ^^^^^^^ - | - = note: this error originates in the attribute macro `simd` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/fearless_simd_tests/tests/ui/simd/fail/non_function.rs b/fearless_simd_tests/tests/ui/simd/fail/non_function.rs index 55e92ec9..9a0a862d 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/non_function.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/non_function.rs @@ -5,5 +5,6 @@ use fearless_simd_macros::simd; #[simd] struct NotAFunction; +//~^ ERROR: expected `fn` fn main() {} diff --git a/fearless_simd_tests/tests/ui/simd/fail/non_function.stderr b/fearless_simd_tests/tests/ui/simd/fail/non_function.stderr deleted file mode 100644 index 54803088..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/non_function.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: expected `fn` - --> tests/ui/simd/fail/non_function.rs:7:1 - | -7 | struct NotAFunction; - | ^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.rs b/fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.rs index 135db6b7..d842db68 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.rs @@ -5,6 +5,7 @@ use fearless_simd_macros::simd; #[simd] fn conditional_token_is_not_supported(#[cfg(any())] simd: S) { + //~^ ERROR: the SIMD token parameter cannot be conditional let _ = simd; } diff --git a/fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.stderr b/fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.stderr deleted file mode 100644 index b6049c92..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/parameter_cfg.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: the SIMD token parameter cannot be conditional - --> tests/ui/simd/fail/parameter_cfg.rs:7:63 - | -7 | fn conditional_token_is_not_supported(#[cfg(any())] simd: S) { - | ^^^^^^^^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/ref_token.rs b/fearless_simd_tests/tests/ui/simd/fail/ref_token.rs index 7dfb8d7b..fec3f2e7 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/ref_token.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/ref_token.rs @@ -5,6 +5,7 @@ use fearless_simd_macros::simd; #[simd] fn ref_token_is_not_supported(ref simd: S) { + //~^ ERROR: the SIMD token parameter must be bound by value, not `ref` let _ = simd; } diff --git a/fearless_simd_tests/tests/ui/simd/fail/ref_token.stderr b/fearless_simd_tests/tests/ui/simd/fail/ref_token.stderr deleted file mode 100644 index 8eab084d..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/ref_token.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: the SIMD token parameter must be bound by value, not `ref` - --> tests/ui/simd/fail/ref_token.rs:7:55 - | -7 | fn ref_token_is_not_supported(ref simd: S) { - | ^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/track_caller.rs b/fearless_simd_tests/tests/ui/simd/fail/track_caller.rs index 7be360d3..796c6e7a 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/track_caller.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/track_caller.rs @@ -5,6 +5,7 @@ use fearless_simd_macros::simd; #[simd] #[track_caller] +//~^ ERROR: `#[simd]` cannot preserve `#[track_caller]` through its closure fn track_caller_is_not_supported(simd: S) { let _ = simd; } diff --git a/fearless_simd_tests/tests/ui/simd/fail/track_caller.stderr b/fearless_simd_tests/tests/ui/simd/fail/track_caller.stderr deleted file mode 100644 index e6411950..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/track_caller.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: `#[simd]` cannot preserve `#[track_caller]` through its closure - --> tests/ui/simd/fail/track_caller.rs:7:1 - | -7 | #[track_caller] - | ^^^^^^^^^^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/fail/variadic.rs b/fearless_simd_tests/tests/ui/simd/fail/variadic.rs index e44394cb..0c8ace13 100644 --- a/fearless_simd_tests/tests/ui/simd/fail/variadic.rs +++ b/fearless_simd_tests/tests/ui/simd/fail/variadic.rs @@ -5,6 +5,7 @@ use fearless_simd_macros::simd; #[simd] unsafe extern "C" fn variadic_is_not_supported(simd: S, _: ...) { + //~^ ERROR: `#[simd]` does not support variadic functions let _ = simd; } diff --git a/fearless_simd_tests/tests/ui/simd/fail/variadic.stderr b/fearless_simd_tests/tests/ui/simd/fail/variadic.stderr deleted file mode 100644 index a6e4d730..00000000 --- a/fearless_simd_tests/tests/ui/simd/fail/variadic.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: `#[simd]` does not support variadic functions - --> tests/ui/simd/fail/variadic.rs:7:81 - | -7 | unsafe extern "C" fn variadic_is_not_supported(simd: S, _: ...) { - | ^^^^^^ diff --git a/fearless_simd_tests/tests/ui/simd/pass/argument_hygiene.rs b/fearless_simd_tests/tests/ui/simd/pass/argument_hygiene.rs index 7d3a233e..ad7b61c1 100644 --- a/fearless_simd_tests/tests/ui/simd/pass/argument_hygiene.rs +++ b/fearless_simd_tests/tests/ui/simd/pass/argument_hygiene.rs @@ -1,6 +1,8 @@ // Copyright 2026 the Fearless_SIMD Authors // SPDX-License-Identifier: Apache-2.0 OR MIT +//@run + #![deny(warnings)] #![forbid(unsafe_code)] diff --git a/fearless_simd_tests/tests/ui/simd/pass/functions.rs b/fearless_simd_tests/tests/ui/simd/pass/functions.rs index ac1caf37..01c48216 100644 --- a/fearless_simd_tests/tests/ui/simd/pass/functions.rs +++ b/fearless_simd_tests/tests/ui/simd/pass/functions.rs @@ -1,6 +1,8 @@ // Copyright 2026 the Fearless_SIMD Authors // SPDX-License-Identifier: Apache-2.0 OR MIT +//@run + use fearless_simd::{Fallback, Simd}; use fearless_simd_macros::simd; diff --git a/fearless_simd_tests/tests/ui/simd/pass/methods.rs b/fearless_simd_tests/tests/ui/simd/pass/methods.rs index 8975a08b..123185e3 100644 --- a/fearless_simd_tests/tests/ui/simd/pass/methods.rs +++ b/fearless_simd_tests/tests/ui/simd/pass/methods.rs @@ -1,6 +1,8 @@ // Copyright 2026 the Fearless_SIMD Authors // SPDX-License-Identifier: Apache-2.0 OR MIT +//@run + use fearless_simd::{Fallback, Simd}; use fearless_simd_macros::simd; diff --git a/fearless_simd_tests/tests/ui/simd/pass/return_types.rs b/fearless_simd_tests/tests/ui/simd/pass/return_types.rs index 4704d9e2..8afb30ea 100644 --- a/fearless_simd_tests/tests/ui/simd/pass/return_types.rs +++ b/fearless_simd_tests/tests/ui/simd/pass/return_types.rs @@ -1,6 +1,8 @@ // Copyright 2026 the Fearless_SIMD Authors // SPDX-License-Identifier: Apache-2.0 OR MIT +//@run + use std::fmt::Display; use fearless_simd::{Fallback, Simd}; From 3a849219ed5d45f537d3616e52bad61cc0ec463f Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 10 Sep 2026 20:40:56 +0100 Subject: [PATCH 20/20] appease the formatter --- fearless_simd_tests/tests/ui/simd/renamed/Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fearless_simd_tests/tests/ui/simd/renamed/Cargo.toml b/fearless_simd_tests/tests/ui/simd/renamed/Cargo.toml index e5878a1e..fede0ec1 100644 --- a/fearless_simd_tests/tests/ui/simd/renamed/Cargo.toml +++ b/fearless_simd_tests/tests/ui/simd/renamed/Cargo.toml @@ -7,5 +7,7 @@ publish = false [workspace] [dependencies] -simd_backend = { package = "fearless_simd", path = "../../../../../fearless_simd", default-features = false, features = ["libm"] } +simd_backend = { package = "fearless_simd", path = "../../../../../fearless_simd", default-features = false, features = [ + "libm", +] } fearless_simd_macros = { path = "../../../../../fearless_simd_macros" }