simdispatch/lib.rs
1//! # simdispatch — SIMD backend selection for the FEC stack
2//!
3//! [`simdispatch`](crate) is the Level 0 crate: the single source for SIMD
4//! capability selection across the FEC ecosystem. Everything else that ships
5//! vector kernels — `fff`, `cafft`, and every future consumer — gets its
6//! backend from here.
7//!
8//! Capability proof comes from [`archmage`](https://github.com/imazen/archmage)
9//! capability tokens: a tier is "on the host" exactly when its token
10//! [`summon`](archmage::SimdToken::summon)s. There is one probe in the whole
11//! stack, and it lives here.
12//!
13//! ## The two primitives
14//!
15//! - [`Backend`] — the ladder. Variants are named after the `archmage` tier
16//! they prove and ordered by that tier's dispatch priority (strongest
17//! first), so the order carries capability and cannot drift from
18//! `archmage`'s source of truth.
19//! - [`Selection`] — a consumer declares the backends it implements
20//! ([`Selection::supports`]) and resolves the choice for the current host:
21//! detected via `summon`, narrowed to the supported set, then adjusted by
22//! the **downgrade-only** `SIMD_BACKEND` override. Refusing to upgrade is a
23//! soundness property: running vector code the CPU cannot execute is
24//! undefined behaviour, not a preference.
25//!
26//! [`backend()`] reports the process-wide resolution of the full ladder
27//! ([`Backend::ALL`]); a consumer with a narrower kernel set builds its own
28//! [`Selection`] instead.
29//!
30//! ## `SIMD_BACKEND`
31//!
32//! The one stack-wide override, replacing the per-crate `FFF_BACKEND` /
33//! `CAFFT_BACKEND` as crates migrate. Accepted values are
34//! [`Backend::name()`] values (`scalar`, `v1`, `v2`, `v3`, `v3_gfni_crypto`, `v4`, `v4x`,
35//! `neon`, `neon_aes`, `wasm128`). It is **downgrade-only**: a request for a
36//! backend the host cannot run, or one stronger than what detection found, is
37//! ignored. The `v4` and `v4x` tiers are available with the `avx512` feature.
38//! `SIMD_BACKEND=scalar` forces the whole stack to portable code —
39//! the escape hatch operators and differential testing need.
40//!
41//! ## `no_std`
42//!
43//! The crate is `#![no_std]`. Without the `std` feature there is no
44//! environment and no runtime detection, so [`backend()`] and any
45//! [`Selection::resolve`] report [`Backend::Scalar`] unconditionally —
46//! matching how the field kernels behave on a std-less build.
47//!
48//! ## Non-scope
49//!
50//! No kernels, no intrinsics, no dispatch tables, no `unsafe`. Consumers keep
51//! every `#[target_feature]` function; this crate only answers which of them
52//! is legal to call on the current host.
53
54#![no_std]
55#![deny(missing_docs)]
56#![deny(unsafe_code)]
57
58#[cfg(feature = "std")]
59extern crate std;
60
61mod backend;
62mod selection;
63
64pub use backend::{Backend, ParseBackendError};
65pub use selection::Selection;
66
67#[cfg(feature = "internals")]
68pub use selection::internals;
69
70#[cfg(feature = "std")]
71use std::sync::LazyLock;
72
73/// The process-wide resolution of the full ladder [`Backend::ALL`], narrowed
74/// to nothing, then adjusted by the `SIMD_BACKEND` override.
75#[cfg(feature = "std")]
76static BACKEND: LazyLock<Backend> = LazyLock::new(|| {
77 Selection::new("SIMD_BACKEND")
78 .supports(Backend::ALL)
79 .resolve()
80});
81
82/// The backend the full ecosystem ladder resolves to on this host, detected
83/// once per process.
84///
85/// This is the *un-narrowed* answer: the strongest [`Backend`] a host that
86/// implemented every tier could use. A consumer that implements only a subset
87/// must resolve its own [`Selection`] rather than reuse this value.
88#[cfg(feature = "std")]
89#[must_use]
90pub fn backend() -> Backend {
91 *BACKEND
92}
93
94/// The backend the full ecosystem ladder resolves to on this host.
95///
96/// Without `std` there is no runtime detection and no environment override,
97/// so this reports [`Backend::Scalar`] unconditionally.
98#[cfg(not(feature = "std"))]
99#[must_use]
100pub fn backend() -> Backend {
101 Backend::Scalar
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn backend_reports_a_member_of_the_ladder() {
110 let backend = backend();
111 assert!(
112 Backend::ALL.contains(&backend),
113 "backend() returned {backend:?}, not in the ladder"
114 );
115 }
116
117 #[cfg(not(feature = "std"))]
118 #[test]
119 fn no_std_reports_scalar() {
120 assert_eq!(backend(), Backend::Scalar);
121 let selected = Selection::new("SIMD_BACKEND")
122 .supports(Backend::ALL)
123 .resolve();
124 assert_eq!(selected, Backend::Scalar);
125 }
126
127 #[cfg(all(feature = "std", target_arch = "x86_64"))]
128 #[test]
129 fn x86_full_ladder_floor_is_v1_without_a_scalar_override() {
130 // V1 (SSE2) always summons on x86_64, so the resolved full ladder must
131 // be an x86 tier — unless `SIMD_BACKEND=scalar` forced the whole
132 // process to portable code, which is exactly the override's job.
133 let backend = backend();
134 if std::env::var("SIMD_BACKEND")
135 .ok()
136 .is_some_and(|v| v == "scalar")
137 {
138 assert_eq!(backend, Backend::Scalar);
139 } else {
140 assert!(
141 matches!(
142 backend,
143 Backend::V4x
144 | Backend::V4
145 | Backend::V3GfniCrypto
146 | Backend::V3
147 | Backend::V2
148 | Backend::V1
149 ),
150 "full ladder resolved to {backend:?} on x86_64"
151 );
152 }
153 }
154
155 #[cfg(all(feature = "std", target_arch = "aarch64"))]
156 #[test]
157 fn aarch64_full_ladder_floor_is_neon_without_a_scalar_override() {
158 let backend = backend();
159 if std::env::var("SIMD_BACKEND")
160 .ok()
161 .is_some_and(|v| v == "scalar")
162 {
163 assert_eq!(backend, Backend::Scalar);
164 } else {
165 assert!(
166 matches!(backend, Backend::NeonAes | Backend::Neon),
167 "full ladder resolved to {backend:?} on aarch64"
168 );
169 }
170 }
171}