1use crate::error::{Error, Result};
17use core::fmt;
18
19pub const BACKEND_ENV_VAR: &str = "HYALITE_BACKEND";
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26#[non_exhaustive]
27pub enum Backend {
28 Scalar,
30 Sse41,
32 Avx2,
34 Neon,
36}
37
38impl Backend {
39 #[must_use]
41 pub const fn name(self) -> &'static str {
42 match self {
43 Backend::Scalar => "scalar",
44 Backend::Sse41 => "sse4.1",
45 Backend::Avx2 => "avx2",
46 Backend::Neon => "neon",
47 }
48 }
49
50 #[must_use]
53 pub(crate) fn simd_lanes(self) -> Option<usize> {
54 match self {
55 Backend::Scalar => None,
56 Backend::Sse41 | Backend::Neon => Some(16),
57 Backend::Avx2 => Some(32),
58 }
59 }
60
61 #[must_use]
68 pub fn is_available(self) -> bool {
69 match self {
70 Backend::Scalar => true,
71 Backend::Sse41 => sse41_detected(),
72 Backend::Avx2 => avx2_detected(),
73 Backend::Neon => neon_available(),
74 }
75 }
76}
77
78#[cfg(target_arch = "x86_64")]
79fn sse41_detected() -> bool {
80 std::is_x86_feature_detected!("sse4.1")
81}
82
83#[cfg(not(target_arch = "x86_64"))]
84fn sse41_detected() -> bool {
85 false
86}
87
88#[cfg(target_arch = "x86_64")]
89fn avx2_detected() -> bool {
90 std::is_x86_feature_detected!("avx2")
91}
92
93#[cfg(not(target_arch = "x86_64"))]
94fn avx2_detected() -> bool {
95 false
96}
97
98#[cfg(target_arch = "aarch64")]
100fn neon_available() -> bool {
101 true
102}
103
104#[cfg(not(target_arch = "aarch64"))]
105fn neon_available() -> bool {
106 false
107}
108
109impl fmt::Display for Backend {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 f.write_str(self.name())
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
118pub enum BackendChoice {
119 #[default]
121 Auto,
122 Force(Backend),
125}
126
127impl BackendChoice {
128 pub fn parse(s: &str) -> Result<Self> {
135 match s.trim().to_ascii_lowercase().as_str() {
136 "auto" => Ok(BackendChoice::Auto),
137 "scalar" => Ok(BackendChoice::Force(Backend::Scalar)),
138 "sse4.1" | "sse41" => Ok(BackendChoice::Force(Backend::Sse41)),
139 "avx2" => Ok(BackendChoice::Force(Backend::Avx2)),
140 "neon" => Ok(BackendChoice::Force(Backend::Neon)),
141 _ => Err(Error::InvalidBackendName {
142 name: s.to_string(),
143 }),
144 }
145 }
146}
147
148fn detect_best() -> Backend {
151 if Backend::Avx2.is_available() {
152 Backend::Avx2
153 } else if Backend::Sse41.is_available() {
154 Backend::Sse41
155 } else if Backend::Neon.is_available() {
156 Backend::Neon
157 } else {
158 Backend::Scalar
159 }
160}
161
162pub(crate) fn resolve(choice: BackendChoice) -> Result<Backend> {
168 match choice {
169 BackendChoice::Auto => Ok(detect_best()),
170 BackendChoice::Force(backend) => {
171 if backend.is_available() {
172 Ok(backend)
173 } else {
174 Err(Error::BackendUnavailable { backend })
175 }
176 }
177 }
178}
179
180pub(crate) fn choice_from_env() -> Result<Option<BackendChoice>> {
187 match std::env::var(BACKEND_ENV_VAR) {
188 Ok(s) if s.trim().is_empty() => Ok(None),
189 Ok(s) => BackendChoice::parse(&s).map(Some),
190 Err(std::env::VarError::NotPresent) => Ok(None),
191 Err(std::env::VarError::NotUnicode(_)) => Err(Error::InvalidBackendName {
192 name: "<non-unicode>".to_string(),
193 }),
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn availability_matches_arch_and_cpu() {
203 assert!(Backend::Scalar.is_available());
205 assert_eq!(Backend::Sse41.is_available(), sse41_detected());
206 assert_eq!(Backend::Avx2.is_available(), avx2_detected());
207 assert_eq!(Backend::Neon.is_available(), neon_available());
208 #[cfg(not(target_arch = "x86_64"))]
209 {
210 assert!(!Backend::Sse41.is_available());
211 assert!(!Backend::Avx2.is_available());
212 }
213 #[cfg(not(target_arch = "aarch64"))]
214 assert!(!Backend::Neon.is_available());
215 }
216
217 #[test]
218 fn name_round_trips_through_parse_for_every_backend() {
219 for b in [
220 Backend::Scalar,
221 Backend::Sse41,
222 Backend::Avx2,
223 Backend::Neon,
224 ] {
225 assert_eq!(
226 BackendChoice::parse(b.name()).unwrap(),
227 BackendChoice::Force(b)
228 );
229 assert_eq!(b.to_string(), b.name());
230 }
231 }
232
233 #[test]
234 fn parse_accepts_aliases_and_is_case_insensitive() {
235 assert_eq!(BackendChoice::parse("auto").unwrap(), BackendChoice::Auto);
236 assert_eq!(BackendChoice::parse("AUTO").unwrap(), BackendChoice::Auto);
237 assert_eq!(
238 BackendChoice::parse(" SSE41 ").unwrap(),
239 BackendChoice::Force(Backend::Sse41)
240 );
241 assert_eq!(
242 BackendChoice::parse("sse4.1").unwrap(),
243 BackendChoice::Force(Backend::Sse41)
244 );
245 assert_eq!(
246 BackendChoice::parse("Avx2").unwrap(),
247 BackendChoice::Force(Backend::Avx2)
248 );
249 }
250
251 #[test]
252 fn parse_rejects_unknown_names() {
253 for bad in ["", "sse2", "ssse3", "avx512", "gpu", "x"] {
254 let err = BackendChoice::parse(bad).unwrap_err();
255 assert_eq!(
256 err,
257 Error::InvalidBackendName {
258 name: bad.to_string()
259 }
260 );
261 }
262 }
263
264 #[test]
265 fn resolve_auto_picks_an_available_backend() {
266 let b = resolve(BackendChoice::Auto).unwrap();
267 assert!(b.is_available());
268 assert_eq!(b, detect_best());
270 }
271
272 #[test]
273 fn resolve_forcing_scalar_always_succeeds() {
274 assert_eq!(
275 resolve(BackendChoice::Force(Backend::Scalar)).unwrap(),
276 Backend::Scalar
277 );
278 }
279
280 #[test]
281 fn resolve_forcing_a_backend_tracks_its_availability() {
282 for b in [Backend::Sse41, Backend::Avx2, Backend::Neon] {
283 let got = resolve(BackendChoice::Force(b));
284 if b.is_available() {
285 assert_eq!(got.unwrap(), b);
286 } else {
287 assert_eq!(got.unwrap_err(), Error::BackendUnavailable { backend: b });
288 }
289 }
290 }
291}