Skip to main content

archmage/tokens/
mod.rs

1//! SIMD capability tokens
2//!
3//! Tokens are zero-sized proof types that demonstrate a CPU feature is available.
4//! They should be obtained via `summon()` and passed through function calls.
5//!
6//! ## Token Availability
7//!
8//! Use `compiled_with()` to check compile-time availability:
9//! - `Some(true)` — Binary was compiled with these features enabled (use `summon().unwrap()`)
10//! - `Some(false)` — Wrong architecture (this token can never be available)
11//! - `None` — Might be available, call `summon()` to check at runtime
12//!
13//! ## Cross-Architecture Design
14//!
15//! All token types exist on all architectures for easier cross-platform code.
16//! On unsupported architectures, `summon()` returns `None` and `compiled_with()`
17//! returns `Some(false)`.
18//!
19//! ## Disabling Tokens
20//!
21//! For testing and benchmarking, tokens can be disabled process-wide:
22//!
23//! ```rust,ignore
24//! // Force scalar fallback for benchmarking
25//! X64V3Token::dangerously_disable_token_process_wide(true)?;
26//! // All summon() calls now return None (cascades to V4, Modern, Fp16)
27//! assert!(X64V3Token::summon().is_none());
28//! ```
29//!
30//! Disabling returns `Err(CompileTimeGuaranteedError)` when the features are
31//! compile-time enabled (e.g., via `-Ctarget-cpu=native`), since the compiler
32//! has already elided the runtime checks.
33
34mod sealed {
35    /// Sealed trait preventing external implementations of [`SimdToken`](super::SimdToken).
36    pub trait Sealed {}
37}
38
39pub use sealed::Sealed;
40
41/// Marker trait for SIMD capability tokens.
42///
43/// All tokens implement this trait, enabling generic code over different
44/// SIMD feature levels.
45///
46/// This trait is **sealed** — it cannot be implemented outside of archmage.
47/// Only tokens created by `summon()` or downcasting are valid.
48///
49/// # Token Lifecycle
50///
51/// 1. Optionally check `compiled_with()` to see if runtime check is needed
52/// 2. Call `summon()` at runtime to detect CPU support
53/// 3. Pass the token through to `#[arcane]` functions — don't forge new ones
54///
55/// # Example
56///
57/// ```rust,ignore
58/// use archmage::{X64V3Token, SimdToken};
59///
60/// fn process(data: &[f32]) -> f32 {
61///     if let Some(token) = X64V3Token::summon() {
62///         return process_avx2(token, data);
63///     }
64///     process_scalar(data)
65/// }
66/// ```
67pub trait SimdToken: sealed::Sealed + Copy + Clone + Send + Sync + 'static {
68    /// Human-readable name for diagnostics and error messages.
69    const NAME: &'static str;
70
71    /// Comma-delimited target features (e.g., `"sse,sse2,avx2,fma,bmi1,bmi2,f16c,lzcnt"`).
72    ///
73    /// All features are listed, including baseline features like SSE/SSE2 for
74    /// x86 tokens. For WASM, this is `"simd128"`.
75    ///
76    /// Empty for [`ScalarToken`].
77    const TARGET_FEATURES: &'static str;
78
79    /// RUSTFLAGS to enable these features at compile time.
80    ///
81    /// Example: `"-Ctarget-feature=+avx2,+fma,+bmi1,+bmi2,+f16c,+lzcnt"`
82    ///
83    /// Empty for [`ScalarToken`].
84    const ENABLE_TARGET_FEATURES: &'static str;
85
86    /// RUSTFLAGS to disable these features at compile time.
87    ///
88    /// Example: `"-Ctarget-feature=-avx2,-fma,-bmi1,-bmi2,-f16c,-lzcnt"`
89    ///
90    /// Useful in [`CompileTimeGuaranteedError`] messages to tell users how
91    /// to recompile so that runtime disable works.
92    ///
93    /// Empty for [`ScalarToken`].
94    const DISABLE_TARGET_FEATURES: &'static str;
95
96    /// Check if this binary was compiled with the required target features enabled.
97    ///
98    /// Returns:
99    /// - `Some(true)` — Features are compile-time enabled (e.g., `-C target-cpu=haswell`)
100    /// - `Some(false)` — Wrong architecture, token can never be available
101    /// - `None` — Might be available, call `summon()` to check at runtime
102    ///
103    /// When `compiled_with()` returns `Some(true)`, `summon().unwrap()` is safe and
104    /// the compiler will elide the runtime check entirely.
105    ///
106    /// # Example
107    ///
108    /// ```rust,ignore
109    /// match X64V3Token::compiled_with() {
110    ///     Some(true) => { /* summon().unwrap() is safe, no runtime check */ }
111    ///     Some(false) => { /* use fallback, this arch can't support it */ }
112    ///     None => { /* call summon() to check at runtime */ }
113    /// }
114    /// ```
115    fn compiled_with() -> Option<bool>;
116
117    /// Deprecated alias for [`compiled_with()`](Self::compiled_with).
118    #[inline(always)]
119    #[deprecated(since = "0.6.0", note = "Use compiled_with() instead")]
120    fn guaranteed() -> Option<bool> {
121        Self::compiled_with()
122    }
123
124    /// Attempt to create a token with runtime feature detection.
125    ///
126    /// Returns `Some(token)` if the CPU supports this feature, `None` otherwise.
127    ///
128    /// # Example
129    ///
130    /// ```rust,ignore
131    /// if let Some(token) = X64V3Token::summon() {
132    ///     // Use AVX2+FMA operations
133    /// } else {
134    ///     // Fallback path
135    /// }
136    /// ```
137    fn summon() -> Option<Self>;
138
139    /// Attempt to create a token with runtime feature detection.
140    ///
141    /// This is an alias for [`summon()`](Self::summon).
142    #[inline(always)]
143    fn attempt() -> Option<Self> {
144        Self::summon()
145    }
146
147    /// Legacy alias for [`summon()`](Self::summon).
148    ///
149    /// **Deprecated:** Use `summon()` instead.
150    #[inline(always)]
151    #[doc(hidden)]
152    fn try_new() -> Option<Self> {
153        Self::summon()
154    }
155
156    /// Create a token without any checks.
157    ///
158    /// # Safety
159    ///
160    /// Caller must guarantee the CPU feature is available. Using a forged token
161    /// when the feature is unavailable causes undefined behavior.
162    ///
163    /// # Deprecated
164    ///
165    /// **Do not use in new code.** Pass tokens through from `summon()` instead.
166    /// If you're inside a `#[cfg(target_feature = "...")]` block where the
167    /// feature is compile-time guaranteed, use `summon().unwrap()`.
168    #[deprecated(
169        since = "0.5.0",
170        note = "Pass tokens through from summon() instead of forging"
171    )]
172    unsafe fn forge_token_dangerously() -> Self;
173}
174
175// All token types, traits, and aliases are generated from token-registry.toml.
176mod generated;
177pub use generated::*;
178
179/// Scalar fallback token — always available on all platforms.
180///
181/// Use this for fallback paths when no SIMD is available. `ScalarToken`
182/// provides type-level proof that "we've given up on SIMD" and allows
183/// consistent API shapes in dispatch code.
184///
185/// # Example
186///
187/// ```rust
188/// use archmage::{ScalarToken, SimdToken};
189///
190/// // Always succeeds
191/// let token = ScalarToken::summon().unwrap();
192///
193/// // Or use the shorthand
194/// let token = ScalarToken;
195/// ```
196#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
197pub struct ScalarToken;
198
199impl sealed::Sealed for ScalarToken {}
200
201impl SimdToken for ScalarToken {
202    const NAME: &'static str = "Scalar";
203    const TARGET_FEATURES: &'static str = "";
204    const ENABLE_TARGET_FEATURES: &'static str = "";
205    const DISABLE_TARGET_FEATURES: &'static str = "";
206
207    /// Always returns `Some(true)` — scalar fallback is always available.
208    #[inline(always)]
209    fn compiled_with() -> Option<bool> {
210        Some(true)
211    }
212
213    /// Always returns `Some(ScalarToken)`.
214    #[inline(always)]
215    fn summon() -> Option<Self> {
216        Some(Self)
217    }
218
219    #[allow(deprecated)]
220    #[inline(always)]
221    unsafe fn forge_token_dangerously() -> Self {
222        Self
223    }
224}
225
226impl ScalarToken {
227    /// Scalar tokens cannot be disabled (they are always available).
228    ///
229    /// Always returns `Err(CompileTimeGuaranteedError)` because `ScalarToken`
230    /// is unconditionally available — there is no runtime check to bypass.
231    pub fn dangerously_disable_token_process_wide(
232        _disabled: bool,
233    ) -> Result<(), CompileTimeGuaranteedError> {
234        Err(CompileTimeGuaranteedError {
235            token_name: Self::NAME,
236            target_features: Self::TARGET_FEATURES,
237            disable_flags: Self::DISABLE_TARGET_FEATURES,
238        })
239    }
240
241    /// Scalar tokens cannot be disabled (they are always available).
242    ///
243    /// Always returns `Err(CompileTimeGuaranteedError)`.
244    pub fn manually_disabled() -> Result<bool, CompileTimeGuaranteedError> {
245        Err(CompileTimeGuaranteedError {
246            token_name: Self::NAME,
247            target_features: Self::TARGET_FEATURES,
248            disable_flags: Self::DISABLE_TARGET_FEATURES,
249        })
250    }
251}
252
253/// Error returned when attempting to disable a token whose features are
254/// compile-time enabled.
255///
256/// When all required features are enabled via `-Ctarget-cpu` or
257/// `-Ctarget-feature`, the compiler has already elided the runtime
258/// detection — `summon()` unconditionally returns `Some`. Disabling
259/// the token would have no effect and silently produce incorrect behavior.
260///
261/// # Resolution
262///
263/// To use `dangerously_disable_token_process_wide`, compile without
264/// the target features enabled. For example:
265/// - Remove `-Ctarget-cpu=native` from `RUSTFLAGS`
266/// - Use the [`disable_flags`](Self::disable_flags) string to disable specific features
267#[derive(Debug, Clone)]
268pub struct CompileTimeGuaranteedError {
269    /// The token type that cannot be disabled.
270    pub token_name: &'static str,
271    /// Comma-delimited target features (e.g., `"avx2,fma,bmi1,bmi2,f16c,lzcnt"`).
272    /// Empty for ScalarToken.
273    pub target_features: &'static str,
274    /// RUSTFLAGS to disable these features (e.g., `"-Ctarget-feature=-avx2,-fma,..."`).
275    /// Empty for ScalarToken.
276    pub disable_flags: &'static str,
277}
278
279impl core::fmt::Display for CompileTimeGuaranteedError {
280    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
281        if self.target_features.is_empty() {
282            write!(f, "Cannot disable {} — always available.", self.token_name)
283        } else {
284            write!(
285                f,
286                "Cannot disable {} — features [{}] are compile-time enabled.\n\n\
287                 To allow runtime disable, recompile with RUSTFLAGS:\n  \
288                 \"{}\"\n\n\
289                 Or remove `-Ctarget-cpu` from RUSTFLAGS entirely.",
290                self.token_name, self.target_features, self.disable_flags
291            )
292        }
293    }
294}
295
296#[cfg(feature = "std")]
297impl std::error::Error for CompileTimeGuaranteedError {}
298
299/// Error returned when [`dangerously_disable_tokens_except_wasm`] fails to
300/// disable one or more tokens because their features are compile-time enabled.
301///
302/// Contains the individual [`CompileTimeGuaranteedError`] for each token that
303/// could not be disabled.
304#[derive(Debug, Clone)]
305pub struct DisableAllSimdError {
306    /// The individual errors for each token that could not be disabled.
307    pub errors: alloc::vec::Vec<CompileTimeGuaranteedError>,
308}
309
310impl core::fmt::Display for DisableAllSimdError {
311    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
312        write!(f, "Failed to disable {} token(s):", self.errors.len())?;
313        for err in &self.errors {
314            write!(f, "\n  - {}", err.token_name)?;
315        }
316        Ok(())
317    }
318}
319
320#[cfg(feature = "std")]
321impl std::error::Error for DisableAllSimdError {}
322
323/// Disable all SIMD tokens process-wide (except WASM, which is always compile-time).
324///
325/// This is a convenience function that calls
326/// [`dangerously_disable_token_process_wide`](X64V2Token::dangerously_disable_token_process_wide)
327/// on each root token for the current architecture. Root tokens cascade to
328/// their descendants, so this disables everything:
329///
330/// - **x86/x86_64:** `X64V2Token` (cascades to V3, V4, Modern, Fp16)
331/// - **AArch64:** `NeonToken` (cascades to Aes, Sha3, Crc)
332///
333/// WASM's `Wasm128Token` is excluded because `simd128` is always a compile-time
334/// decision on `wasm32` — there is no runtime detection to bypass.
335///
336/// Pass `disabled = false` to re-enable all tokens.
337///
338/// # Errors
339///
340/// Returns [`DisableAllSimdError`] if any token's features are compile-time
341/// enabled (e.g., via `-Ctarget-cpu=native`). Tokens that *can* be disabled
342/// are still disabled; only the failures are collected.
343///
344/// # Example
345///
346/// ```rust,ignore
347/// use archmage::dangerously_disable_tokens_except_wasm;
348///
349/// // Force scalar fallbacks for benchmarking
350/// dangerously_disable_tokens_except_wasm(true)?;
351/// // ... run benchmarks ...
352/// dangerously_disable_tokens_except_wasm(false)?;
353/// ```
354pub fn dangerously_disable_tokens_except_wasm(disabled: bool) -> Result<(), DisableAllSimdError> {
355    let mut errors = alloc::vec::Vec::new();
356
357    // x86/x86_64: disable V2 (cascades to V3, V4, Modern, Fp16)
358    #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
359    if let Err(e) = X64V2Token::dangerously_disable_token_process_wide(disabled) {
360        errors.push(e);
361    }
362
363    // AArch64: disable NEON (cascades to Aes, Sha3, Crc)
364    #[cfg(target_arch = "aarch64")]
365    if let Err(e) = NeonToken::dangerously_disable_token_process_wide(disabled) {
366        errors.push(e);
367    }
368
369    if errors.is_empty() {
370        Ok(())
371    } else {
372        Err(DisableAllSimdError { errors })
373    }
374}
375
376/// Trait for compile-time dispatch via monomorphization.
377///
378/// Each concrete token implements this trait, returning `Some(self)` for its
379/// own type and `None` for others. The compiler monomorphizes away all the
380/// `None` branches, leaving only the matching path.
381///
382/// # Example
383///
384/// ```rust
385/// use archmage::{IntoConcreteToken, SimdToken, ScalarToken};
386///
387/// fn dispatch<T: IntoConcreteToken>(token: T, data: &[f32]) -> f32 {
388///     // Compiler eliminates non-matching branches
389///     if let Some(_t) = token.as_scalar() {
390///         return data.iter().sum();
391///     }
392///     // ... other paths for x64v3, neon, etc.
393///     0.0
394/// }
395///
396/// let result = dispatch(ScalarToken, &[1.0, 2.0, 3.0]);
397/// assert_eq!(result, 6.0);
398/// ```
399pub trait IntoConcreteToken: SimdToken + Sized {
400    /// Try to cast to X64V1Token.
401    #[inline(always)]
402    fn as_x64v1(self) -> Option<X64V1Token> {
403        None
404    }
405
406    /// Try to cast to X64V2Token.
407    #[inline(always)]
408    fn as_x64v2(self) -> Option<X64V2Token> {
409        None
410    }
411
412    /// Try to cast to X64CryptoToken.
413    #[inline(always)]
414    fn as_x64_crypto(self) -> Option<X64CryptoToken> {
415        None
416    }
417
418    /// Try to cast to X64V3Token.
419    #[inline(always)]
420    fn as_x64v3(self) -> Option<X64V3Token> {
421        None
422    }
423
424    /// Try to cast to X64V3CryptoToken.
425    #[inline(always)]
426    fn as_x64v3_crypto(self) -> Option<X64V3CryptoToken> {
427        None
428    }
429
430    /// Try to cast to X64V4Token.
431    #[inline(always)]
432    fn as_x64v4(self) -> Option<X64V4Token> {
433        None
434    }
435
436    /// Try to cast to X64V4xToken.
437    #[inline(always)]
438    fn as_x64v4x(self) -> Option<X64V4xToken> {
439        None
440    }
441
442    /// Try to cast to Avx512Fp16Token.
443    #[inline(always)]
444    fn as_avx512_fp16(self) -> Option<Avx512Fp16Token> {
445        None
446    }
447
448    /// Try to cast to NeonToken.
449    #[inline(always)]
450    fn as_neon(self) -> Option<NeonToken> {
451        None
452    }
453
454    /// Try to cast to NeonAesToken.
455    #[inline(always)]
456    fn as_neon_aes(self) -> Option<NeonAesToken> {
457        None
458    }
459
460    /// Try to cast to NeonSha3Token.
461    #[inline(always)]
462    fn as_neon_sha3(self) -> Option<NeonSha3Token> {
463        None
464    }
465
466    /// Try to cast to NeonCrcToken.
467    #[inline(always)]
468    fn as_neon_crc(self) -> Option<NeonCrcToken> {
469        None
470    }
471
472    /// Try to cast to Arm64V2Token.
473    #[inline(always)]
474    fn as_arm_v2(self) -> Option<Arm64V2Token> {
475        None
476    }
477
478    /// Try to cast to Arm64V3Token.
479    #[inline(always)]
480    fn as_arm_v3(self) -> Option<Arm64V3Token> {
481        None
482    }
483
484    /// Try to cast to Wasm128Token.
485    #[inline(always)]
486    fn as_wasm128(self) -> Option<Wasm128Token> {
487        None
488    }
489
490    /// Try to cast to ScalarToken.
491    #[inline(always)]
492    fn as_scalar(self) -> Option<ScalarToken> {
493        None
494    }
495}
496
497// Implement IntoConcreteToken for ScalarToken
498impl IntoConcreteToken for ScalarToken {
499    #[inline(always)]
500    fn as_scalar(self) -> Option<ScalarToken> {
501        Some(self)
502    }
503}
504
505// Implement IntoConcreteToken for X64V1Token
506impl IntoConcreteToken for X64V1Token {
507    #[inline(always)]
508    fn as_x64v1(self) -> Option<X64V1Token> {
509        Some(self)
510    }
511}
512
513// Implement IntoConcreteToken for X64V2Token
514impl IntoConcreteToken for X64V2Token {
515    #[inline(always)]
516    fn as_x64v2(self) -> Option<X64V2Token> {
517        Some(self)
518    }
519}
520
521// Implement IntoConcreteToken for X64CryptoToken
522impl IntoConcreteToken for X64CryptoToken {
523    #[inline(always)]
524    fn as_x64_crypto(self) -> Option<X64CryptoToken> {
525        Some(self)
526    }
527}
528
529// Implement IntoConcreteToken for X64V3Token
530impl IntoConcreteToken for X64V3Token {
531    #[inline(always)]
532    fn as_x64v3(self) -> Option<X64V3Token> {
533        Some(self)
534    }
535}
536
537// Implement IntoConcreteToken for X64V3CryptoToken
538impl IntoConcreteToken for X64V3CryptoToken {
539    #[inline(always)]
540    fn as_x64v3_crypto(self) -> Option<X64V3CryptoToken> {
541        Some(self)
542    }
543}
544
545// Implement IntoConcreteToken for X64V4Token
546impl IntoConcreteToken for X64V4Token {
547    #[inline(always)]
548    fn as_x64v4(self) -> Option<X64V4Token> {
549        Some(self)
550    }
551}
552
553// Implement IntoConcreteToken for X64V4xToken
554impl IntoConcreteToken for X64V4xToken {
555    #[inline(always)]
556    fn as_x64v4x(self) -> Option<X64V4xToken> {
557        Some(self)
558    }
559}
560
561// Implement IntoConcreteToken for Avx512Fp16Token
562impl IntoConcreteToken for Avx512Fp16Token {
563    #[inline(always)]
564    fn as_avx512_fp16(self) -> Option<Avx512Fp16Token> {
565        Some(self)
566    }
567}
568
569// Implement IntoConcreteToken for NeonToken
570impl IntoConcreteToken for NeonToken {
571    #[inline(always)]
572    fn as_neon(self) -> Option<NeonToken> {
573        Some(self)
574    }
575}
576
577// Implement IntoConcreteToken for NeonAesToken
578impl IntoConcreteToken for NeonAesToken {
579    #[inline(always)]
580    fn as_neon_aes(self) -> Option<NeonAesToken> {
581        Some(self)
582    }
583}
584
585// Implement IntoConcreteToken for NeonSha3Token
586impl IntoConcreteToken for NeonSha3Token {
587    #[inline(always)]
588    fn as_neon_sha3(self) -> Option<NeonSha3Token> {
589        Some(self)
590    }
591}
592
593// Implement IntoConcreteToken for NeonCrcToken
594impl IntoConcreteToken for NeonCrcToken {
595    #[inline(always)]
596    fn as_neon_crc(self) -> Option<NeonCrcToken> {
597        Some(self)
598    }
599}
600
601// Implement IntoConcreteToken for Arm64V2Token
602impl IntoConcreteToken for Arm64V2Token {
603    #[inline(always)]
604    fn as_arm_v2(self) -> Option<Arm64V2Token> {
605        Some(self)
606    }
607}
608
609// Implement IntoConcreteToken for Arm64V3Token
610impl IntoConcreteToken for Arm64V3Token {
611    #[inline(always)]
612    fn as_arm_v3(self) -> Option<Arm64V3Token> {
613        Some(self)
614    }
615}
616
617// Implement IntoConcreteToken for Wasm128Token
618impl IntoConcreteToken for Wasm128Token {
619    #[inline(always)]
620    fn as_wasm128(self) -> Option<Wasm128Token> {
621        Some(self)
622    }
623}