Skip to main content

archmage_macros/
lib.rs

1//! Proc-macros for archmage SIMD capability tokens.
2//!
3//! Provides `#[arcane]`, `#[rite]`, `#[autoversion]`, `incant!`, and `#[magetypes]`.
4
5mod arcane;
6mod autoversion;
7mod common;
8mod generated;
9mod incant;
10mod magetypes;
11mod rite;
12mod tiers;
13mod token_discovery;
14
15use proc_macro::TokenStream;
16use syn::parse_macro_input;
17
18use arcane::*;
19use autoversion::*;
20use common::*;
21use incant::*;
22use magetypes::*;
23use rite::*;
24use tiers::*;
25
26// Re-export items used by the test module (via `use super::*`).
27#[cfg(test)]
28use generated::{token_to_features, trait_to_features};
29#[cfg(test)]
30use quote::{ToTokens, format_ident};
31#[cfg(test)]
32use syn::{FnArg, PatType, Type};
33#[cfg(test)]
34use token_discovery::*;
35
36// LightFn, filter_inline_attrs, is_lint_attr, filter_lint_attrs, gen_cfg_guard,
37// build_turbofish, replace_self_in_tokens, suffix_path → moved to common.rs
38// ArcaneArgs, SelfReceiver, arcane_impl, arcane_impl_* → moved to arcane.rs
39// generate_imports → moved to common.rs
40
41/// Mark a function as an arcane SIMD function.
42///
43/// This macro generates a safe wrapper around a `#[target_feature]` function.
44/// The token parameter type determines which CPU features are enabled.
45///
46/// # Expansion Modes
47///
48/// ## Sibling (default)
49///
50/// Generates two functions at the same scope: a safe `#[target_feature]` sibling
51/// and a safe wrapper. `self`/`Self` work naturally since both functions share scope.
52/// Compatible with `#![forbid(unsafe_code)]`.
53///
54/// ```ignore
55/// #[arcane]
56/// fn process(token: X64V3Token, data: &[f32; 8]) -> [f32; 8] { /* body */ }
57/// // Expands to (x86_64 only):
58/// #[cfg(target_arch = "x86_64")]
59/// #[doc(hidden)]
60/// #[target_feature(enable = "avx2,fma,...")]
61/// fn __arcane_process(token: X64V3Token, data: &[f32; 8]) -> [f32; 8] { /* body */ }
62///
63/// #[cfg(target_arch = "x86_64")]
64/// fn process(token: X64V3Token, data: &[f32; 8]) -> [f32; 8] {
65///     unsafe { __arcane_process(token, data) }
66/// }
67/// ```
68///
69/// Methods work naturally:
70///
71/// ```ignore
72/// impl MyType {
73///     #[arcane]
74///     fn compute(&self, token: X64V3Token) -> f32 {
75///         self.data.iter().sum()  // self/Self just work!
76///     }
77/// }
78/// ```
79///
80/// ## Nested (`nested` or `_self = Type`)
81///
82/// Generates a nested inner function inside the original. Required for trait impls
83/// (where sibling functions would fail) and when `_self = Type` is used.
84///
85/// ```ignore
86/// impl SimdOps for MyType {
87///     #[arcane(_self = MyType)]
88///     fn compute(&self, token: X64V3Token) -> Self {
89///         // Use _self instead of self, Self replaced with MyType
90///         _self.data.iter().sum()
91///     }
92/// }
93/// ```
94///
95/// # Cross-Architecture Behavior
96///
97/// **Default (cfg-out):** On the wrong architecture, the function is not emitted
98/// at all — no stub, no dead code. Code that references it must be cfg-gated.
99///
100/// **With `stub`:** Generates an `unreachable!()` stub on wrong architectures.
101/// Use when cross-arch dispatch references the function without cfg guards.
102///
103/// ```ignore
104/// #[arcane(stub)]  // generates stub on wrong arch
105/// fn process_neon(token: NeonToken, data: &[f32]) -> f32 { ... }
106/// ```
107///
108/// `incant!` is unaffected — it already cfg-gates dispatch calls by architecture.
109///
110/// # Token Parameter Forms
111///
112/// ```ignore
113/// // Concrete token
114/// #[arcane]
115/// fn process(token: X64V3Token, data: &[f32; 8]) -> [f32; 8] { ... }
116///
117/// // impl Trait bound
118/// #[arcane]
119/// fn process(token: impl HasX64V2, data: &[f32; 8]) -> [f32; 8] { ... }
120///
121/// // Generic with inline or where-clause bounds
122/// #[arcane]
123/// fn process<T: HasX64V2>(token: T, data: &[f32; 8]) -> [f32; 8] { ... }
124///
125/// // Wildcard
126/// #[arcane]
127/// fn process(_: X64V3Token, data: &[f32; 8]) -> [f32; 8] { ... }
128/// ```
129///
130/// # Options
131///
132/// | Option | Effect |
133/// |--------|--------|
134/// | `stub` | Generate `unreachable!()` stub on wrong architecture |
135/// | `nested` | Use nested inner function instead of sibling |
136/// | `_self = Type` | Implies `nested`, transforms self receiver, replaces Self |
137/// | `inline_always` | Use `#[inline(always)]` (requires nightly) |
138/// | `import_intrinsics` | Auto-import `archmage::intrinsics::{arch}::*` (includes safe memory ops) |
139/// | `import_magetypes` | Auto-import `magetypes::simd::{ns}::*` and `magetypes::simd::backends::*` |
140///
141/// ## Auto-Imports
142///
143/// `import_intrinsics` and `import_magetypes` inject `use` statements into the
144/// function body, eliminating boilerplate. The macro derives the architecture and
145/// namespace from the token type:
146///
147/// ```ignore
148/// // Without auto-imports — lots of boilerplate:
149/// use std::arch::x86_64::*;
150/// use magetypes::simd::v3::*;
151///
152/// #[arcane]
153/// fn process(token: X64V3Token, data: &[f32; 8]) -> f32 {
154///     let v = f32x8::load(token, data);
155///     let zero = _mm256_setzero_ps();
156///     // ...
157/// }
158///
159/// // With auto-imports — clean:
160/// #[arcane(import_intrinsics, import_magetypes)]
161/// fn process(token: X64V3Token, data: &[f32; 8]) -> f32 {
162///     let v = f32x8::load(token, data);
163///     let zero = _mm256_setzero_ps();
164///     // ...
165/// }
166/// ```
167///
168/// The namespace mapping is token-driven:
169///
170/// | Token | `import_intrinsics` | `import_magetypes` |
171/// |-------|--------------------|--------------------|
172/// | `X64V1..V3Token` | `archmage::intrinsics::x86_64::*` | `magetypes::simd::v3::*` |
173/// | `X64V4Token` | `archmage::intrinsics::x86_64::*` | `magetypes::simd::v4::*` |
174/// | `X64V4xToken` | `archmage::intrinsics::x86_64::*` | `magetypes::simd::v4x::*` |
175/// | `NeonToken` / ARM | `archmage::intrinsics::aarch64::*` | `magetypes::simd::neon::*` |
176/// | `Wasm128Token` | `archmage::intrinsics::wasm32::*` | `magetypes::simd::wasm128::*` |
177///
178/// Works with concrete tokens, `impl Trait` bounds, and generic parameters.
179///
180/// # Supported Tokens
181///
182/// - **x86_64**: `X64V2Token`, `X64V3Token`/`Desktop64`, `X64V4Token`/`Avx512Token`/`Server64`,
183///   `X64V4xToken`, `Avx512Fp16Token`, `X64CryptoToken`, `X64V3CryptoToken`
184/// - **ARM**: `NeonToken`/`Arm64`, `Arm64V2Token`, `Arm64V3Token`,
185///   `NeonAesToken`, `NeonSha3Token`, `NeonCrcToken`
186/// - **WASM**: `Wasm128Token`
187///
188/// # Supported Trait Bounds
189///
190/// `HasX64V2`, `HasX64V4`, `HasNeon`, `HasNeonAes`, `HasNeonSha3`, `HasArm64V2`, `HasArm64V3`
191///
192/// ```ignore
193/// #![feature(target_feature_inline_always)]
194///
195/// #[arcane(inline_always)]
196/// fn fast_kernel(token: Avx2Token, data: &mut [f32]) {
197///     // Inner function will use #[inline(always)]
198/// }
199/// ```
200#[proc_macro_attribute]
201pub fn arcane(attr: TokenStream, item: TokenStream) -> TokenStream {
202    let args = parse_macro_input!(attr as ArcaneArgs);
203    let input_fn = parse_macro_input!(item as LightFn);
204    arcane_impl(input_fn, "arcane", args)
205}
206
207/// Legacy alias for [`arcane`].
208///
209/// **Deprecated:** Use `#[arcane]` instead. This alias exists only for migration.
210#[proc_macro_attribute]
211#[doc(hidden)]
212pub fn simd_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
213    let args = parse_macro_input!(attr as ArcaneArgs);
214    let input_fn = parse_macro_input!(item as LightFn);
215    arcane_impl(input_fn, "simd_fn", args)
216}
217
218/// Descriptive alias for [`arcane`].
219///
220/// Generates a safe wrapper around a `#[target_feature]` inner function.
221/// The token type in your signature determines which CPU features are enabled.
222/// Creates an LLVM optimization boundary — use [`token_target_features`]
223/// (alias for [`rite`]) for inner helpers to avoid this.
224///
225/// Since Rust 1.85, value-based SIMD intrinsics are safe inside
226/// `#[target_feature]` functions. This macro generates the `#[target_feature]`
227/// wrapper so you never need to write `unsafe` for SIMD code.
228///
229/// See [`arcane`] for full documentation and examples.
230#[proc_macro_attribute]
231pub fn token_target_features_boundary(attr: TokenStream, item: TokenStream) -> TokenStream {
232    let args = parse_macro_input!(attr as ArcaneArgs);
233    let input_fn = parse_macro_input!(item as LightFn);
234    arcane_impl(input_fn, "token_target_features_boundary", args)
235}
236
237// ============================================================================
238// Rite macro for inner SIMD functions (inlines into matching #[target_feature] callers)
239// ============================================================================
240
241/// Annotate inner SIMD helpers called from `#[arcane]` functions.
242///
243/// Unlike `#[arcane]`, which creates an inner `#[target_feature]` function behind
244/// a safe boundary, `#[rite]` adds `#[target_feature]` and `#[inline]` directly.
245/// LLVM inlines it into any caller with matching features — no boundary crossing.
246///
247/// # Three Modes
248///
249/// **Token-based:** Reads the token type from the function signature.
250/// ```ignore
251/// #[rite]
252/// fn helper(_: X64V3Token, v: __m256) -> __m256 { _mm256_add_ps(v, v) }
253/// ```
254///
255/// **Tier-based:** Specify the tier name directly, no token parameter needed.
256/// ```ignore
257/// #[rite(v3)]
258/// fn helper(v: __m256) -> __m256 { _mm256_add_ps(v, v) }
259/// ```
260///
261/// Both produce identical code. The token form can be easier to remember if
262/// you already have the token in scope.
263///
264/// **Multi-tier:** Specify multiple tiers to generate suffixed variants.
265/// ```ignore
266/// #[rite(v3, v4)]
267/// fn process(data: &[f32; 4]) -> f32 { data.iter().sum() }
268/// // Generates: process_v3() and process_v4()
269/// ```
270///
271/// Each variant gets its own `#[target_feature]` and `#[cfg(target_arch)]`.
272/// Since Rust 1.85, calling these from a matching `#[arcane]` or `#[rite]`
273/// context is safe — no `unsafe` needed when the caller has matching or
274/// superset features.
275///
276/// # Safety
277///
278/// `#[rite]` functions can only be safely called from contexts where the
279/// required CPU features are enabled:
280/// - From within `#[arcane]` functions with matching/superset tokens
281/// - From within other `#[rite]` functions with matching/superset tokens
282/// - From code compiled with `-Ctarget-cpu` that enables the features
283///
284/// Calling from other contexts requires `unsafe` and the caller must ensure
285/// the CPU supports the required features.
286///
287/// # Cross-Architecture Behavior
288///
289/// Like `#[arcane]`, defaults to cfg-out (no function on wrong arch).
290/// Use `#[rite(stub)]` to generate an unreachable stub instead.
291///
292/// # Options
293///
294/// | Option | Effect |
295/// |--------|--------|
296/// | tier name(s) | `v3`, `neon`, etc. One = single function; multiple = suffixed variants |
297/// | `stub` | Generate `unreachable!()` stub on wrong architecture |
298/// | `import_intrinsics` | Auto-import `archmage::intrinsics::{arch}::*` (includes safe memory ops) |
299/// | `import_magetypes` | Auto-import `magetypes::simd::{ns}::*` and `magetypes::simd::backends::*` |
300///
301/// See `#[arcane]` docs for the full namespace mapping table.
302///
303/// # Comparison with #[arcane]
304///
305/// | Aspect | `#[arcane]` | `#[rite]` |
306/// |--------|-------------|-----------|
307/// | Creates wrapper | Yes | No |
308/// | Entry point | Yes | No |
309/// | Inlines into caller | No (barrier) | Yes |
310/// | Safe to call anywhere | Yes (with token) | Only from feature-enabled context |
311/// | Multi-tier variants | No | Yes (`#[rite(v3, v4, neon)]`) |
312/// | `stub` param | Yes | Yes |
313/// | `import_intrinsics` | Yes | Yes |
314/// | `import_magetypes` | Yes | Yes |
315#[proc_macro_attribute]
316pub fn rite(attr: TokenStream, item: TokenStream) -> TokenStream {
317    let args = parse_macro_input!(attr as RiteArgs);
318    let input_fn = parse_macro_input!(item as LightFn);
319    rite_impl(input_fn, args)
320}
321
322/// Descriptive alias for [`rite`].
323///
324/// Applies `#[target_feature]` + `#[inline]` based on the token type in your
325/// function signature. No wrapper, no optimization boundary. Use for functions
326/// called from within `#[arcane]`/`#[token_target_features_boundary]` code.
327///
328/// Since Rust 1.85, calling a `#[target_feature]` function from another function
329/// with matching features is safe — no `unsafe` needed.
330///
331/// See [`rite`] for full documentation and examples.
332#[proc_macro_attribute]
333pub fn token_target_features(attr: TokenStream, item: TokenStream) -> TokenStream {
334    let args = parse_macro_input!(attr as RiteArgs);
335    let input_fn = parse_macro_input!(item as LightFn);
336    rite_impl(input_fn, args)
337}
338
339// RiteArgs, rite_impl, rite_single_impl, rite_multi_tier_impl → moved to rite.rs
340
341// =============================================================================
342// magetypes! macro - generate platform variants from generic function
343// =============================================================================
344
345/// Generate platform-specific variants from a function by replacing `Token`.
346///
347/// Use `Token` as a placeholder for the token type. The macro generates
348/// suffixed variants with `Token` replaced by the concrete token type, and
349/// each variant wrapped in the appropriate `#[cfg(target_arch = ...)]` guard.
350///
351/// # Default tiers
352///
353/// Without arguments, generates `_v3`, `_v4`, `_neon`, `_wasm128`, `_scalar`:
354///
355/// ```rust,ignore
356/// #[magetypes]
357/// fn process(token: Token, data: &[f32]) -> f32 {
358///     inner_simd_work(token, data)
359/// }
360/// ```
361///
362/// # Explicit tiers
363///
364/// Specify which tiers to generate:
365///
366/// ```rust,ignore
367/// #[magetypes(v1, v3, neon)]
368/// fn process(token: Token, data: &[f32]) -> f32 {
369///     inner_simd_work(token, data)
370/// }
371/// // Generates: process_v1, process_v3, process_neon, process_scalar
372/// ```
373///
374/// `scalar` is always included implicitly.
375///
376/// Known tiers: `v1`, `v2`, `v3`, `v4`, `v4x`, `neon`, `neon_aes`,
377/// `neon_sha3`, `neon_crc`, `wasm128`, `wasm128_relaxed`, `scalar`.
378///
379/// # What gets replaced
380///
381/// **Only `Token`** is replaced — with the concrete token type for each variant
382/// (e.g., `archmage::X64V3Token`, `archmage::ScalarToken`). SIMD types like
383/// `f32x8` and constants like `LANES` are **not** replaced by this macro.
384///
385/// # Usage with incant!
386///
387/// The generated variants work with `incant!` for dispatch:
388///
389/// ```rust,ignore
390/// pub fn process_api(data: &[f32]) -> f32 {
391///     incant!(process(data))
392/// }
393///
394/// // Or with matching explicit tiers:
395/// pub fn process_api(data: &[f32]) -> f32 {
396///     incant!(process(data), [v1, v3, neon, scalar])
397/// }
398/// ```
399#[proc_macro_attribute]
400pub fn magetypes(attr: TokenStream, item: TokenStream) -> TokenStream {
401    let input_fn = parse_macro_input!(item as LightFn);
402
403    // Parse optional tier list from attribute args: tier1, tier2(feature), ...
404    let tier_names: Vec<String> = if attr.is_empty() {
405        DEFAULT_TIER_NAMES.iter().map(|s| s.to_string()).collect()
406    } else {
407        match syn::parse::Parser::parse(parse_tier_names, attr) {
408            Ok(names) => names,
409            Err(e) => return e.to_compile_error().into(),
410        }
411    };
412
413    // default_optional: tiers with cfg_feature are optional by default
414    let tiers = match resolve_tiers(
415        &tier_names,
416        input_fn.sig.ident.span(),
417        true, // magetypes always uses default_optional for cfg_feature tiers
418    ) {
419        Ok(t) => t,
420        Err(e) => return e.to_compile_error().into(),
421    };
422
423    magetypes_impl(input_fn, &tiers)
424}
425
426// =============================================================================
427// incant! macro - dispatch to platform-specific variants
428// =============================================================================
429// incant! macro - dispatch to platform-specific variants
430// =============================================================================
431
432/// Dispatch to platform-specific SIMD variants.
433///
434/// # Entry Point Mode (no token yet)
435///
436/// Summons tokens and dispatches to the best available variant:
437///
438/// ```rust,ignore
439/// pub fn public_api(data: &[f32]) -> f32 {
440///     incant!(dot(data))
441/// }
442/// ```
443///
444/// Expands to runtime feature detection + dispatch to `dot_v3`, `dot_v4`,
445/// `dot_neon`, `dot_wasm128`, or `dot_scalar`.
446///
447/// # Explicit Tiers
448///
449/// Specify which tiers to dispatch to:
450///
451/// ```rust,ignore
452/// // Only dispatch to v1, v3, neon, and scalar
453/// pub fn api(data: &[f32]) -> f32 {
454///     incant!(process(data), [v1, v3, neon, scalar])
455/// }
456/// ```
457///
458/// Always include `scalar` in explicit tier lists — `incant!` always
459/// emits a `fn_scalar()` call as the final fallback, and listing it
460/// documents this dependency. Currently auto-appended if omitted;
461/// will become a compile error in v1.0. Unknown tier names cause a
462/// compile error. Tiers are automatically sorted into correct
463/// dispatch order (highest priority first).
464///
465/// Known tiers: `v1`, `v2`, `v3`, `v4`, `v4x`, `neon`, `neon_aes`,
466/// `neon_sha3`, `neon_crc`, `wasm128`, `wasm128_relaxed`, `scalar`.
467///
468/// # Passthrough Mode (already have token)
469///
470/// Uses compile-time dispatch via `IntoConcreteToken`:
471///
472/// ```rust,ignore
473/// #[arcane]
474/// fn outer(token: X64V3Token, data: &[f32]) -> f32 {
475///     incant!(inner(data) with token)
476/// }
477/// ```
478///
479/// Also supports explicit tiers:
480///
481/// ```rust,ignore
482/// fn inner<T: IntoConcreteToken>(token: T, data: &[f32]) -> f32 {
483///     incant!(process(data) with token, [v3, neon, scalar])
484/// }
485/// ```
486///
487/// The compiler monomorphizes the dispatch, eliminating non-matching branches.
488///
489/// # Variant Naming
490///
491/// Functions must have suffixed variants matching the selected tiers:
492/// - `_v1` for `X64V1Token`
493/// - `_v2` for `X64V2Token`
494/// - `_v3` for `X64V3Token`
495/// - `_v4` for `X64V4Token` (requires `avx512` feature)
496/// - `_v4x` for `X64V4xToken` (requires `avx512` feature)
497/// - `_neon` for `NeonToken`
498/// - `_neon_aes` for `NeonAesToken`
499/// - `_neon_sha3` for `NeonSha3Token`
500/// - `_neon_crc` for `NeonCrcToken`
501/// - `_wasm128` for `Wasm128Token`
502/// - `_scalar` for `ScalarToken`
503#[proc_macro]
504pub fn incant(input: TokenStream) -> TokenStream {
505    let input = parse_macro_input!(input as IncantInput);
506    incant_impl(input)
507}
508
509/// Legacy alias for [`incant!`].
510#[proc_macro]
511pub fn simd_route(input: TokenStream) -> TokenStream {
512    let input = parse_macro_input!(input as IncantInput);
513    incant_impl(input)
514}
515
516/// Descriptive alias for [`incant!`].
517///
518/// Dispatches to architecture-specific function variants at runtime.
519/// Looks for suffixed functions (`_v3`, `_v4`, `_neon`, `_wasm128`, `_scalar`)
520/// and calls the best one the CPU supports.
521///
522/// See [`incant!`] for full documentation and examples.
523#[proc_macro]
524pub fn dispatch_variant(input: TokenStream) -> TokenStream {
525    let input = parse_macro_input!(input as IncantInput);
526    incant_impl(input)
527}
528
529// =============================================================================
530
531/// Let the compiler auto-vectorize scalar code for each architecture.
532///
533/// Write a plain scalar function and let `#[autoversion]` generate
534/// architecture-specific copies — each compiled with different
535/// `#[target_feature]` flags via `#[arcane]` — plus a runtime dispatcher
536/// that calls the best one the CPU supports.
537///
538/// # Quick start
539///
540/// ```rust,ignore
541/// use archmage::autoversion;
542///
543/// #[autoversion]
544/// fn sum_of_squares(data: &[f32]) -> f32 {
545///     let mut sum = 0.0f32;
546///     for &x in data {
547///         sum += x * x;
548///     }
549///     sum
550/// }
551///
552/// // Call directly — no token, no unsafe:
553/// let result = sum_of_squares(&my_data);
554/// ```
555///
556/// Each variant gets `#[arcane]` → `#[target_feature(enable = "avx2,fma,...")]`,
557/// which unlocks the compiler's auto-vectorizer for that feature set.
558/// On x86-64, that loop compiles to `vfmadd231ps`. On aarch64, `fmla`.
559/// The `_scalar` fallback compiles without SIMD target features.
560///
561/// # SimdToken — optional placeholder
562///
563/// You can optionally write `_token: SimdToken` as a parameter. The macro
564/// recognizes it and strips it from the dispatcher — both forms produce
565/// identical output. Prefer the tokenless form for new code.
566///
567/// ```rust,ignore
568/// #[autoversion]
569/// fn normalize(_token: SimdToken, data: &mut [f32], scale: f32) {
570///     for x in data.iter_mut() { *x = (*x - 128.0) * scale; }
571/// }
572/// // Dispatcher is: fn normalize(data: &mut [f32], scale: f32)
573/// ```
574///
575/// # What gets generated
576///
577/// `#[autoversion] fn process(data: &[f32]) -> f32` expands to:
578///
579/// - `process_v4(token: X64V4Token, ...)` — AVX-512
580/// - `process_v3(token: X64V3Token, ...)` — AVX2+FMA
581/// - `process_neon(token: NeonToken, ...)` — aarch64 NEON
582/// - `process_wasm128(token: Wasm128Token, ...)` — WASM SIMD
583/// - `process_scalar(token: ScalarToken, ...)` — no SIMD, always available
584/// - `process(data: &[f32]) -> f32` — **dispatcher**
585///
586/// Variants are private. The dispatcher gets the original function's visibility.
587/// Within the same module, call variants directly for testing or benchmarking.
588///
589/// # Explicit tiers
590///
591/// ```rust,ignore
592/// #[autoversion(v3, v4, neon, arm_v2, wasm128)]
593/// fn process(data: &[f32]) -> f32 { ... }
594/// ```
595///
596/// `scalar` is always included implicitly.
597///
598/// Default tiers: `v4`, `v3`, `neon`, `wasm128`, `scalar`.
599///
600/// Known tiers: `v1`, `v2`, `v3`, `v3_crypto`, `v4`, `v4x`, `neon`,
601/// `neon_aes`, `neon_sha3`, `neon_crc`, `arm_v2`, `arm_v3`, `wasm128`,
602/// `wasm128_relaxed`, `x64_crypto`, `scalar`.
603///
604/// # Methods
605///
606/// For inherent methods, `self` works naturally:
607///
608/// ```rust,ignore
609/// impl ImageBuffer {
610///     #[autoversion]
611///     fn normalize(&mut self, gamma: f32) {
612///         for pixel in &mut self.data {
613///             *pixel = (*pixel / 255.0).powf(gamma);
614///         }
615///     }
616/// }
617/// buffer.normalize(2.2);
618/// ```
619///
620/// For trait method delegation, use `_self = Type` (nested mode):
621///
622/// ```rust,ignore
623/// impl MyType {
624///     #[autoversion(_self = MyType)]
625///     fn compute_impl(&self, data: &[f32]) -> f32 {
626///         _self.weights.iter().zip(data).map(|(w, d)| w * d).sum()
627///     }
628/// }
629/// ```
630///
631/// # Nesting with `incant!`
632///
633/// Hand-written SIMD for specific tiers, autoversion for the rest:
634///
635/// ```rust,ignore
636/// pub fn process(data: &[f32]) -> f32 {
637///     incant!(process(data), [v4, scalar])
638/// }
639///
640/// #[arcane(import_intrinsics)]
641/// fn process_v4(_t: X64V4Token, data: &[f32]) -> f32 { /* AVX-512 */ }
642///
643/// // Bridge: incant! passes ScalarToken, autoversion doesn't need one
644/// fn process_scalar(_: ScalarToken, data: &[f32]) -> f32 {
645///     process_auto(data)
646/// }
647///
648/// #[autoversion(v3, neon)]
649/// fn process_auto(data: &[f32]) -> f32 { data.iter().sum() }
650/// ```
651///
652/// # Comparison with `#[magetypes]` + `incant!`
653///
654/// | | `#[autoversion]` | `#[magetypes]` + `incant!` |
655/// |---|---|---|
656/// | Generates variants + dispatcher | Yes | Variants only (+ separate `incant!`) |
657/// | Body touched | No (signature only) | Yes (text substitution) |
658/// | Best for | Scalar auto-vectorization | Hand-written SIMD types |
659#[proc_macro_attribute]
660pub fn autoversion(attr: TokenStream, item: TokenStream) -> TokenStream {
661    let args = parse_macro_input!(attr as AutoversionArgs);
662    let input_fn = parse_macro_input!(item as LightFn);
663    autoversion_impl(input_fn, args)
664}
665
666// =============================================================================
667// Unit tests for token/trait recognition maps
668// =============================================================================
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673
674    use super::generated::{ALL_CONCRETE_TOKENS, ALL_TRAIT_NAMES};
675    use syn::{ItemFn, ReturnType};
676
677    #[test]
678    fn every_concrete_token_is_in_token_to_features() {
679        for &name in ALL_CONCRETE_TOKENS {
680            assert!(
681                token_to_features(name).is_some(),
682                "Token `{}` exists in runtime crate but is NOT recognized by \
683                 token_to_features() in the proc macro. Add it!",
684                name
685            );
686        }
687    }
688
689    #[test]
690    fn every_trait_is_in_trait_to_features() {
691        for &name in ALL_TRAIT_NAMES {
692            assert!(
693                trait_to_features(name).is_some(),
694                "Trait `{}` exists in runtime crate but is NOT recognized by \
695                 trait_to_features() in the proc macro. Add it!",
696                name
697            );
698        }
699    }
700
701    #[test]
702    fn token_aliases_map_to_same_features() {
703        // Desktop64 = X64V3Token
704        assert_eq!(
705            token_to_features("Desktop64"),
706            token_to_features("X64V3Token"),
707            "Desktop64 and X64V3Token should map to identical features"
708        );
709
710        // Server64 = X64V4Token = Avx512Token
711        assert_eq!(
712            token_to_features("Server64"),
713            token_to_features("X64V4Token"),
714            "Server64 and X64V4Token should map to identical features"
715        );
716        assert_eq!(
717            token_to_features("X64V4Token"),
718            token_to_features("Avx512Token"),
719            "X64V4Token and Avx512Token should map to identical features"
720        );
721
722        // Arm64 = NeonToken
723        assert_eq!(
724            token_to_features("Arm64"),
725            token_to_features("NeonToken"),
726            "Arm64 and NeonToken should map to identical features"
727        );
728    }
729
730    #[test]
731    fn trait_to_features_includes_tokens_as_bounds() {
732        // Tier tokens should also work as trait bounds
733        // (for `impl X64V3Token` patterns, even though Rust won't allow it,
734        // the macro processes AST before type checking)
735        let tier_tokens = [
736            "X64V2Token",
737            "X64CryptoToken",
738            "X64V3Token",
739            "Desktop64",
740            "Avx2FmaToken",
741            "X64V4Token",
742            "Avx512Token",
743            "Server64",
744            "X64V4xToken",
745            "Avx512Fp16Token",
746            "NeonToken",
747            "Arm64",
748            "NeonAesToken",
749            "NeonSha3Token",
750            "NeonCrcToken",
751            "Arm64V2Token",
752            "Arm64V3Token",
753        ];
754
755        for &name in &tier_tokens {
756            assert!(
757                trait_to_features(name).is_some(),
758                "Tier token `{}` should also be recognized in trait_to_features() \
759                 for use as a generic bound. Add it!",
760                name
761            );
762        }
763    }
764
765    #[test]
766    fn trait_features_are_cumulative() {
767        // HasX64V4 should include all HasX64V2 features plus more
768        let v2_features = trait_to_features("HasX64V2").unwrap();
769        let v4_features = trait_to_features("HasX64V4").unwrap();
770
771        for &f in v2_features {
772            assert!(
773                v4_features.contains(&f),
774                "HasX64V4 should include v2 feature `{}` but doesn't",
775                f
776            );
777        }
778
779        // v4 should have more features than v2
780        assert!(
781            v4_features.len() > v2_features.len(),
782            "HasX64V4 should have more features than HasX64V2"
783        );
784    }
785
786    #[test]
787    fn x64v3_trait_features_include_v2() {
788        // X64V3Token as trait bound should include v2 features
789        let v2 = trait_to_features("HasX64V2").unwrap();
790        let v3 = trait_to_features("X64V3Token").unwrap();
791
792        for &f in v2 {
793            assert!(
794                v3.contains(&f),
795                "X64V3Token trait features should include v2 feature `{}` but don't",
796                f
797            );
798        }
799    }
800
801    #[test]
802    fn has_neon_aes_includes_neon() {
803        let neon = trait_to_features("HasNeon").unwrap();
804        let neon_aes = trait_to_features("HasNeonAes").unwrap();
805
806        for &f in neon {
807            assert!(
808                neon_aes.contains(&f),
809                "HasNeonAes should include NEON feature `{}`",
810                f
811            );
812        }
813    }
814
815    #[test]
816    fn no_removed_traits_are_recognized() {
817        // These traits were removed in 0.3.0 and should NOT be recognized
818        let removed = [
819            "HasSse",
820            "HasSse2",
821            "HasSse41",
822            "HasSse42",
823            "HasAvx",
824            "HasAvx2",
825            "HasFma",
826            "HasAvx512f",
827            "HasAvx512bw",
828            "HasAvx512vl",
829            "HasAvx512vbmi2",
830            "HasSve",
831            "HasSve2",
832        ];
833
834        for &name in &removed {
835            assert!(
836                trait_to_features(name).is_none(),
837                "Removed trait `{}` should NOT be in trait_to_features(). \
838                 It was removed in 0.3.0 — users should migrate to tier traits.",
839                name
840            );
841        }
842    }
843
844    #[test]
845    fn no_nonexistent_tokens_are_recognized() {
846        // These tokens don't exist and should NOT be recognized
847        let fake = [
848            "SveToken",
849            "Sve2Token",
850            "Avx512VnniToken",
851            "X64V4ModernToken",
852            "NeonFp16Token",
853        ];
854
855        for &name in &fake {
856            assert!(
857                token_to_features(name).is_none(),
858                "Non-existent token `{}` should NOT be in token_to_features()",
859                name
860            );
861        }
862    }
863
864    #[test]
865    fn featureless_traits_are_not_in_registries() {
866        // SimdToken and IntoConcreteToken should NOT be in any feature registry
867        // because they don't map to CPU features
868        for &name in FEATURELESS_TRAIT_NAMES {
869            assert!(
870                token_to_features(name).is_none(),
871                "`{}` should NOT be in token_to_features() — it has no CPU features",
872                name
873            );
874            assert!(
875                trait_to_features(name).is_none(),
876                "`{}` should NOT be in trait_to_features() — it has no CPU features",
877                name
878            );
879        }
880    }
881
882    #[test]
883    fn find_featureless_trait_detects_simdtoken() {
884        let names = vec!["SimdToken".to_string()];
885        assert_eq!(find_featureless_trait(&names), Some("SimdToken"));
886
887        let names = vec!["IntoConcreteToken".to_string()];
888        assert_eq!(find_featureless_trait(&names), Some("IntoConcreteToken"));
889
890        // Feature-bearing traits should NOT be detected
891        let names = vec!["HasX64V2".to_string()];
892        assert_eq!(find_featureless_trait(&names), None);
893
894        let names = vec!["HasNeon".to_string()];
895        assert_eq!(find_featureless_trait(&names), None);
896
897        // Mixed: if SimdToken is among real traits, still detected
898        let names = vec!["SimdToken".to_string(), "HasX64V2".to_string()];
899        assert_eq!(find_featureless_trait(&names), Some("SimdToken"));
900    }
901
902    #[test]
903    fn arm64_v2_v3_traits_are_cumulative() {
904        let v2_features = trait_to_features("HasArm64V2").unwrap();
905        let v3_features = trait_to_features("HasArm64V3").unwrap();
906
907        for &f in v2_features {
908            assert!(
909                v3_features.contains(&f),
910                "HasArm64V3 should include v2 feature `{}` but doesn't",
911                f
912            );
913        }
914
915        assert!(
916            v3_features.len() > v2_features.len(),
917            "HasArm64V3 should have more features than HasArm64V2"
918        );
919    }
920
921    // =========================================================================
922    // resolve_tiers — additive / subtractive / override
923    // =========================================================================
924
925    fn resolve_tier_names(names: &[&str], default_gates: bool) -> Vec<String> {
926        let names: Vec<String> = names.iter().map(|s| s.to_string()).collect();
927        resolve_tiers(&names, proc_macro2::Span::call_site(), default_gates)
928            .unwrap()
929            .iter()
930            .map(|rt| {
931                if let Some(ref gate) = rt.feature_gate {
932                    format!("{}({})", rt.name, gate)
933                } else {
934                    rt.name.to_string()
935                }
936            })
937            .collect()
938    }
939
940    #[test]
941    fn resolve_defaults() {
942        let tiers = resolve_tier_names(&["v4", "v3", "neon", "wasm128", "scalar"], true);
943        assert!(tiers.contains(&"v3".to_string()));
944        assert!(tiers.contains(&"scalar".to_string()));
945        // v4 gets auto-gated when default_feature_gates=true
946        assert!(tiers.contains(&"v4(avx512)".to_string()));
947    }
948
949    #[test]
950    fn resolve_additive_appends() {
951        let tiers = resolve_tier_names(&["+v1"], true);
952        assert!(tiers.contains(&"v1".to_string()));
953        assert!(tiers.contains(&"v3".to_string())); // from defaults
954        assert!(tiers.contains(&"scalar".to_string())); // from defaults
955    }
956
957    #[test]
958    fn resolve_additive_v4_overrides_gate() {
959        // +v4 should replace v4(avx512) with plain v4 (no gate)
960        let tiers = resolve_tier_names(&["+v4"], true);
961        assert!(tiers.contains(&"v4".to_string())); // no gate
962        assert!(!tiers.iter().any(|t| t == "v4(avx512)")); // gated version gone
963    }
964
965    #[test]
966    fn resolve_additive_default_replaces_scalar() {
967        let tiers = resolve_tier_names(&["+default"], true);
968        assert!(tiers.contains(&"default".to_string()));
969        assert!(!tiers.iter().any(|t| t == "scalar")); // scalar replaced
970    }
971
972    #[test]
973    fn resolve_subtractive_removes() {
974        let tiers = resolve_tier_names(&["-neon", "-wasm128"], true);
975        assert!(!tiers.iter().any(|t| t == "neon"));
976        assert!(!tiers.iter().any(|t| t == "wasm128"));
977        assert!(tiers.contains(&"v3".to_string())); // others remain
978    }
979
980    #[test]
981    fn resolve_mixed_add_remove() {
982        let tiers = resolve_tier_names(&["-neon", "-wasm128", "+v1"], true);
983        assert!(tiers.contains(&"v1".to_string()));
984        assert!(!tiers.iter().any(|t| t == "neon"));
985        assert!(!tiers.iter().any(|t| t == "wasm128"));
986        assert!(tiers.contains(&"v3".to_string()));
987        assert!(tiers.contains(&"scalar".to_string()));
988    }
989
990    #[test]
991    fn resolve_additive_duplicate_is_noop() {
992        // +v3 when v3 is already in defaults — no duplicate
993        let tiers = resolve_tier_names(&["+v3"], true);
994        let v3_count = tiers.iter().filter(|t| t.as_str() == "v3").count();
995        assert_eq!(v3_count, 1);
996    }
997
998    #[test]
999    fn resolve_mixing_plus_and_plain_is_error() {
1000        let names: Vec<String> = vec!["+v1".into(), "v3".into()];
1001        let result = resolve_tiers(&names, proc_macro2::Span::call_site(), true);
1002        assert!(result.is_err());
1003    }
1004
1005    #[test]
1006    fn resolve_underscore_tier_name() {
1007        let tiers = resolve_tier_names(&["_v3", "_neon", "_scalar"], false);
1008        assert!(tiers.contains(&"v3".to_string()));
1009        assert!(tiers.contains(&"neon".to_string()));
1010        assert!(tiers.contains(&"scalar".to_string()));
1011    }
1012
1013    // =========================================================================
1014    // autoversion — argument parsing
1015    // =========================================================================
1016
1017    #[test]
1018    fn autoversion_args_empty() {
1019        let args: AutoversionArgs = syn::parse_str("").unwrap();
1020        assert!(args.self_type.is_none());
1021        assert!(args.tiers.is_none());
1022    }
1023
1024    #[test]
1025    fn autoversion_args_single_tier() {
1026        let args: AutoversionArgs = syn::parse_str("v3").unwrap();
1027        assert!(args.self_type.is_none());
1028        assert_eq!(args.tiers.as_ref().unwrap(), &["v3"]);
1029    }
1030
1031    #[test]
1032    fn autoversion_args_tiers_only() {
1033        let args: AutoversionArgs = syn::parse_str("v3, v4, neon").unwrap();
1034        assert!(args.self_type.is_none());
1035        let tiers = args.tiers.unwrap();
1036        assert_eq!(tiers, vec!["v3", "v4", "neon"]);
1037    }
1038
1039    #[test]
1040    fn autoversion_args_many_tiers() {
1041        let args: AutoversionArgs =
1042            syn::parse_str("v1, v2, v3, v4, v4x, neon, arm_v2, wasm128").unwrap();
1043        assert_eq!(
1044            args.tiers.unwrap(),
1045            vec!["v1", "v2", "v3", "v4", "v4x", "neon", "arm_v2", "wasm128"]
1046        );
1047    }
1048
1049    #[test]
1050    fn autoversion_args_trailing_comma() {
1051        let args: AutoversionArgs = syn::parse_str("v3, v4,").unwrap();
1052        assert_eq!(args.tiers.as_ref().unwrap(), &["v3", "v4"]);
1053    }
1054
1055    #[test]
1056    fn autoversion_args_self_only() {
1057        let args: AutoversionArgs = syn::parse_str("_self = MyType").unwrap();
1058        assert!(args.self_type.is_some());
1059        assert!(args.tiers.is_none());
1060    }
1061
1062    #[test]
1063    fn autoversion_args_self_and_tiers() {
1064        let args: AutoversionArgs = syn::parse_str("_self = MyType, v3, neon").unwrap();
1065        assert!(args.self_type.is_some());
1066        let tiers = args.tiers.unwrap();
1067        assert_eq!(tiers, vec!["v3", "neon"]);
1068    }
1069
1070    #[test]
1071    fn autoversion_args_tiers_then_self() {
1072        // _self can appear after tier names
1073        let args: AutoversionArgs = syn::parse_str("v3, neon, _self = MyType").unwrap();
1074        assert!(args.self_type.is_some());
1075        let tiers = args.tiers.unwrap();
1076        assert_eq!(tiers, vec!["v3", "neon"]);
1077    }
1078
1079    #[test]
1080    fn autoversion_args_self_with_path_type() {
1081        let args: AutoversionArgs = syn::parse_str("_self = crate::MyType").unwrap();
1082        assert!(args.self_type.is_some());
1083        assert!(args.tiers.is_none());
1084    }
1085
1086    #[test]
1087    fn autoversion_args_self_with_generic_type() {
1088        let args: AutoversionArgs = syn::parse_str("_self = Vec<u8>").unwrap();
1089        assert!(args.self_type.is_some());
1090        let ty_str = args.self_type.unwrap().to_token_stream().to_string();
1091        assert!(ty_str.contains("Vec"), "Expected Vec<u8>, got: {}", ty_str);
1092    }
1093
1094    #[test]
1095    fn autoversion_args_self_trailing_comma() {
1096        let args: AutoversionArgs = syn::parse_str("_self = MyType,").unwrap();
1097        assert!(args.self_type.is_some());
1098        assert!(args.tiers.is_none());
1099    }
1100
1101    // =========================================================================
1102    // autoversion — find_autoversion_token_param
1103    // =========================================================================
1104
1105    #[test]
1106    fn find_autoversion_token_param_simdtoken_first() {
1107        let f: ItemFn =
1108            syn::parse_str("fn process(token: SimdToken, data: &[f32]) -> f32 {}").unwrap();
1109        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1110        assert_eq!(param.index, 0);
1111        assert_eq!(param.ident, "token");
1112        assert_eq!(param.kind, AutoversionTokenKind::SimdToken);
1113    }
1114
1115    #[test]
1116    fn find_autoversion_token_param_simdtoken_second() {
1117        let f: ItemFn =
1118            syn::parse_str("fn process(data: &[f32], token: SimdToken) -> f32 {}").unwrap();
1119        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1120        assert_eq!(param.index, 1);
1121        assert_eq!(param.kind, AutoversionTokenKind::SimdToken);
1122    }
1123
1124    #[test]
1125    fn find_autoversion_token_param_underscore_prefix() {
1126        let f: ItemFn =
1127            syn::parse_str("fn process(_token: SimdToken, data: &[f32]) -> f32 {}").unwrap();
1128        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1129        assert_eq!(param.index, 0);
1130        assert_eq!(param.ident, "_token");
1131    }
1132
1133    #[test]
1134    fn find_autoversion_token_param_wildcard() {
1135        let f: ItemFn = syn::parse_str("fn process(_: SimdToken, data: &[f32]) -> f32 {}").unwrap();
1136        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1137        assert_eq!(param.index, 0);
1138        assert_eq!(param.ident, "__autoversion_token");
1139    }
1140
1141    #[test]
1142    fn find_autoversion_token_param_scalar_token() {
1143        let f: ItemFn =
1144            syn::parse_str("fn process_scalar(_: ScalarToken, data: &[f32]) -> f32 {}").unwrap();
1145        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1146        assert_eq!(param.index, 0);
1147        assert_eq!(param.kind, AutoversionTokenKind::ScalarToken);
1148    }
1149
1150    #[test]
1151    fn find_autoversion_token_param_not_found() {
1152        let f: ItemFn = syn::parse_str("fn process(data: &[f32]) -> f32 {}").unwrap();
1153        assert!(find_autoversion_token_param(&f.sig).unwrap().is_none());
1154    }
1155
1156    #[test]
1157    fn find_autoversion_token_param_no_params() {
1158        let f: ItemFn = syn::parse_str("fn process() {}").unwrap();
1159        assert!(find_autoversion_token_param(&f.sig).unwrap().is_none());
1160    }
1161
1162    #[test]
1163    fn find_autoversion_token_param_concrete_token_errors() {
1164        let f: ItemFn =
1165            syn::parse_str("fn process(token: X64V3Token, data: &[f32]) -> f32 {}").unwrap();
1166        let err = find_autoversion_token_param(&f.sig).unwrap_err();
1167        let msg = err.to_string();
1168        assert!(
1169            msg.contains("concrete token"),
1170            "error should mention concrete token: {msg}"
1171        );
1172        assert!(
1173            msg.contains("#[arcane]"),
1174            "error should suggest #[arcane]: {msg}"
1175        );
1176    }
1177
1178    #[test]
1179    fn find_autoversion_token_param_neon_token_errors() {
1180        let f: ItemFn =
1181            syn::parse_str("fn process(token: NeonToken, data: &[f32]) -> f32 {}").unwrap();
1182        assert!(find_autoversion_token_param(&f.sig).is_err());
1183    }
1184
1185    #[test]
1186    fn find_autoversion_token_param_unknown_type_ignored() {
1187        // Random types are just regular params, not token params
1188        let f: ItemFn = syn::parse_str("fn process(data: &[f32], scale: f32) -> f32 {}").unwrap();
1189        assert!(find_autoversion_token_param(&f.sig).unwrap().is_none());
1190    }
1191
1192    #[test]
1193    fn find_autoversion_token_param_among_many() {
1194        let f: ItemFn = syn::parse_str(
1195            "fn process(a: i32, b: f64, token: SimdToken, c: &str, d: bool) -> f32 {}",
1196        )
1197        .unwrap();
1198        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1199        assert_eq!(param.index, 2);
1200        assert_eq!(param.ident, "token");
1201    }
1202
1203    #[test]
1204    fn find_autoversion_token_param_with_generics() {
1205        let f: ItemFn =
1206            syn::parse_str("fn process<T: Clone>(token: SimdToken, data: &[T]) -> T {}").unwrap();
1207        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1208        assert_eq!(param.index, 0);
1209    }
1210
1211    #[test]
1212    fn find_autoversion_token_param_with_where_clause() {
1213        let f: ItemFn = syn::parse_str(
1214            "fn process<T>(token: SimdToken, data: &[T]) -> T where T: Copy + Default {}",
1215        )
1216        .unwrap();
1217        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1218        assert_eq!(param.index, 0);
1219    }
1220
1221    #[test]
1222    fn find_autoversion_token_param_with_lifetime() {
1223        let f: ItemFn =
1224            syn::parse_str("fn process<'a>(token: SimdToken, data: &'a [f32]) -> &'a f32 {}")
1225                .unwrap();
1226        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1227        assert_eq!(param.index, 0);
1228    }
1229
1230    // =========================================================================
1231    // autoversion — tier resolution
1232    // =========================================================================
1233
1234    #[test]
1235    fn autoversion_default_tiers_all_resolve() {
1236        let names: Vec<String> = DEFAULT_TIER_NAMES.iter().map(|s| s.to_string()).collect();
1237        let tiers = resolve_tiers(&names, proc_macro2::Span::call_site(), false).unwrap();
1238        assert!(!tiers.is_empty());
1239        // scalar should be present
1240        assert!(tiers.iter().any(|t| t.name == "scalar"));
1241    }
1242
1243    #[test]
1244    fn autoversion_scalar_always_appended() {
1245        let names = vec!["v3".to_string(), "neon".to_string()];
1246        let tiers = resolve_tiers(&names, proc_macro2::Span::call_site(), false).unwrap();
1247        assert!(
1248            tiers.iter().any(|t| t.name == "scalar"),
1249            "scalar must be auto-appended"
1250        );
1251    }
1252
1253    #[test]
1254    fn autoversion_scalar_not_duplicated() {
1255        let names = vec!["v3".to_string(), "scalar".to_string()];
1256        let tiers = resolve_tiers(&names, proc_macro2::Span::call_site(), false).unwrap();
1257        let scalar_count = tiers.iter().filter(|t| t.name == "scalar").count();
1258        assert_eq!(scalar_count, 1, "scalar must not be duplicated");
1259    }
1260
1261    #[test]
1262    fn autoversion_tiers_sorted_by_priority() {
1263        let names = vec!["neon".to_string(), "v4".to_string(), "v3".to_string()];
1264        let tiers = resolve_tiers(&names, proc_macro2::Span::call_site(), false).unwrap();
1265        // v4 (priority 40) > v3 (30) > neon (20) > scalar (0)
1266        let priorities: Vec<u32> = tiers.iter().map(|t| t.priority).collect();
1267        for window in priorities.windows(2) {
1268            assert!(
1269                window[0] >= window[1],
1270                "Tiers not sorted by priority: {:?}",
1271                priorities
1272            );
1273        }
1274    }
1275
1276    #[test]
1277    fn autoversion_unknown_tier_errors() {
1278        let names = vec!["v3".to_string(), "avx9000".to_string()];
1279        let result = resolve_tiers(&names, proc_macro2::Span::call_site(), false);
1280        match result {
1281            Ok(_) => panic!("Expected error for unknown tier 'avx9000'"),
1282            Err(e) => {
1283                let err_msg = e.to_string();
1284                assert!(
1285                    err_msg.contains("avx9000"),
1286                    "Error should mention unknown tier: {}",
1287                    err_msg
1288                );
1289            }
1290        }
1291    }
1292
1293    #[test]
1294    fn autoversion_all_known_tiers_resolve() {
1295        // Every tier in ALL_TIERS should be findable
1296        for tier in ALL_TIERS {
1297            assert!(
1298                find_tier(tier.name).is_some(),
1299                "Tier '{}' should be findable by name",
1300                tier.name
1301            );
1302        }
1303    }
1304
1305    #[test]
1306    fn autoversion_default_tier_list_is_sensible() {
1307        // Defaults should cover x86, ARM, WASM, and scalar
1308        let names: Vec<String> = DEFAULT_TIER_NAMES.iter().map(|s| s.to_string()).collect();
1309        let tiers = resolve_tiers(&names, proc_macro2::Span::call_site(), false).unwrap();
1310
1311        let has_x86 = tiers.iter().any(|t| t.target_arch == Some("x86_64"));
1312        let has_arm = tiers.iter().any(|t| t.target_arch == Some("aarch64"));
1313        let has_wasm = tiers.iter().any(|t| t.target_arch == Some("wasm32"));
1314        let has_scalar = tiers.iter().any(|t| t.name == "scalar");
1315
1316        assert!(has_x86, "Default tiers should include an x86_64 tier");
1317        assert!(has_arm, "Default tiers should include an aarch64 tier");
1318        assert!(has_wasm, "Default tiers should include a wasm32 tier");
1319        assert!(has_scalar, "Default tiers should include scalar");
1320    }
1321
1322    // =========================================================================
1323    // autoversion — variant replacement (AST manipulation)
1324    // =========================================================================
1325
1326    /// Mirrors what `autoversion_impl` does for a single variant: parse an
1327    /// ItemFn (for test convenience), rename it, swap the SimdToken param
1328    /// type, optionally inject the `_self` preamble for scalar+self.
1329    fn do_variant_replacement(func: &str, tier_name: &str, has_self: bool) -> ItemFn {
1330        let mut f: ItemFn = syn::parse_str(func).unwrap();
1331        let fn_name = f.sig.ident.to_string();
1332
1333        let tier = find_tier(tier_name).unwrap();
1334
1335        // Rename
1336        f.sig.ident = format_ident!("{}_{}", fn_name, tier.suffix);
1337
1338        // Find and replace SimdToken param type (skip for "default" — tokenless)
1339        let token_idx = find_autoversion_token_param(&f.sig)
1340            .expect("should not error on SimdToken")
1341            .unwrap_or_else(|| panic!("No SimdToken param in: {}", func))
1342            .index;
1343        if tier_name == "default" {
1344            // Remove the token param for default tier
1345            let stmts = f.block.stmts.clone();
1346            let mut inputs: Vec<FnArg> = f.sig.inputs.iter().cloned().collect();
1347            inputs.remove(token_idx);
1348            f.sig.inputs = inputs.into_iter().collect();
1349            f.block.stmts = stmts;
1350        } else {
1351            let concrete_type: Type = syn::parse_str(tier.token_path).unwrap();
1352            if let FnArg::Typed(pt) = &mut f.sig.inputs[token_idx] {
1353                *pt.ty = concrete_type;
1354            }
1355        }
1356
1357        // Fallback (scalar/default) + self: inject preamble
1358        if (tier_name == "scalar" || tier_name == "default") && has_self {
1359            let preamble: syn::Stmt = syn::parse_quote!(let _self = self;);
1360            f.block.stmts.insert(0, preamble);
1361        }
1362
1363        f
1364    }
1365
1366    #[test]
1367    fn variant_replacement_v3_renames_function() {
1368        let f = do_variant_replacement(
1369            "fn process(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1370            "v3",
1371            false,
1372        );
1373        assert_eq!(f.sig.ident, "process_v3");
1374    }
1375
1376    #[test]
1377    fn variant_replacement_v3_replaces_token_type() {
1378        let f = do_variant_replacement(
1379            "fn process(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1380            "v3",
1381            false,
1382        );
1383        let first_param_ty = match &f.sig.inputs[0] {
1384            FnArg::Typed(pt) => pt.ty.to_token_stream().to_string(),
1385            _ => panic!("Expected typed param"),
1386        };
1387        assert!(
1388            first_param_ty.contains("X64V3Token"),
1389            "Expected X64V3Token, got: {}",
1390            first_param_ty
1391        );
1392    }
1393
1394    #[test]
1395    fn variant_replacement_neon_produces_valid_fn() {
1396        let f = do_variant_replacement(
1397            "fn compute(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1398            "neon",
1399            false,
1400        );
1401        assert_eq!(f.sig.ident, "compute_neon");
1402        let first_param_ty = match &f.sig.inputs[0] {
1403            FnArg::Typed(pt) => pt.ty.to_token_stream().to_string(),
1404            _ => panic!("Expected typed param"),
1405        };
1406        assert!(
1407            first_param_ty.contains("NeonToken"),
1408            "Expected NeonToken, got: {}",
1409            first_param_ty
1410        );
1411    }
1412
1413    #[test]
1414    fn variant_replacement_wasm128_produces_valid_fn() {
1415        let f = do_variant_replacement(
1416            "fn compute(_t: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1417            "wasm128",
1418            false,
1419        );
1420        assert_eq!(f.sig.ident, "compute_wasm128");
1421    }
1422
1423    #[test]
1424    fn variant_replacement_scalar_produces_valid_fn() {
1425        let f = do_variant_replacement(
1426            "fn compute(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1427            "scalar",
1428            false,
1429        );
1430        assert_eq!(f.sig.ident, "compute_scalar");
1431        let first_param_ty = match &f.sig.inputs[0] {
1432            FnArg::Typed(pt) => pt.ty.to_token_stream().to_string(),
1433            _ => panic!("Expected typed param"),
1434        };
1435        assert!(
1436            first_param_ty.contains("ScalarToken"),
1437            "Expected ScalarToken, got: {}",
1438            first_param_ty
1439        );
1440    }
1441
1442    #[test]
1443    fn variant_replacement_v4_produces_valid_fn() {
1444        let f = do_variant_replacement(
1445            "fn transform(token: SimdToken, data: &mut [f32]) { }",
1446            "v4",
1447            false,
1448        );
1449        assert_eq!(f.sig.ident, "transform_v4");
1450        let first_param_ty = match &f.sig.inputs[0] {
1451            FnArg::Typed(pt) => pt.ty.to_token_stream().to_string(),
1452            _ => panic!("Expected typed param"),
1453        };
1454        assert!(
1455            first_param_ty.contains("X64V4Token"),
1456            "Expected X64V4Token, got: {}",
1457            first_param_ty
1458        );
1459    }
1460
1461    #[test]
1462    fn variant_replacement_v4x_produces_valid_fn() {
1463        let f = do_variant_replacement(
1464            "fn transform(token: SimdToken, data: &mut [f32]) { }",
1465            "v4x",
1466            false,
1467        );
1468        assert_eq!(f.sig.ident, "transform_v4x");
1469    }
1470
1471    #[test]
1472    fn variant_replacement_arm_v2_produces_valid_fn() {
1473        let f = do_variant_replacement(
1474            "fn transform(token: SimdToken, data: &mut [f32]) { }",
1475            "arm_v2",
1476            false,
1477        );
1478        assert_eq!(f.sig.ident, "transform_arm_v2");
1479    }
1480
1481    #[test]
1482    fn variant_replacement_preserves_generics() {
1483        let f = do_variant_replacement(
1484            "fn process<T: Copy + Default>(token: SimdToken, data: &[T]) -> T { T::default() }",
1485            "v3",
1486            false,
1487        );
1488        assert_eq!(f.sig.ident, "process_v3");
1489        // Generic params should still be present
1490        assert!(
1491            !f.sig.generics.params.is_empty(),
1492            "Generics should be preserved"
1493        );
1494    }
1495
1496    #[test]
1497    fn variant_replacement_preserves_where_clause() {
1498        let f = do_variant_replacement(
1499            "fn process<T>(token: SimdToken, data: &[T]) -> T where T: Copy + Default { T::default() }",
1500            "v3",
1501            false,
1502        );
1503        assert!(
1504            f.sig.generics.where_clause.is_some(),
1505            "Where clause should be preserved"
1506        );
1507    }
1508
1509    #[test]
1510    fn variant_replacement_preserves_return_type() {
1511        let f = do_variant_replacement(
1512            "fn process(token: SimdToken, data: &[f32]) -> Vec<f32> { vec![] }",
1513            "neon",
1514            false,
1515        );
1516        let ret = f.sig.output.to_token_stream().to_string();
1517        assert!(
1518            ret.contains("Vec"),
1519            "Return type should be preserved, got: {}",
1520            ret
1521        );
1522    }
1523
1524    #[test]
1525    fn variant_replacement_preserves_multiple_params() {
1526        let f = do_variant_replacement(
1527            "fn process(token: SimdToken, a: &[f32], b: &[f32], scale: f32) -> f32 { 0.0 }",
1528            "v3",
1529            false,
1530        );
1531        // SimdToken → X64V3Token, plus the 3 other params
1532        assert_eq!(f.sig.inputs.len(), 4);
1533    }
1534
1535    #[test]
1536    fn variant_replacement_preserves_no_return_type() {
1537        let f = do_variant_replacement(
1538            "fn transform(token: SimdToken, data: &mut [f32]) { }",
1539            "v3",
1540            false,
1541        );
1542        assert!(
1543            matches!(f.sig.output, ReturnType::Default),
1544            "No return type should remain as Default"
1545        );
1546    }
1547
1548    #[test]
1549    fn variant_replacement_preserves_lifetime_params() {
1550        let f = do_variant_replacement(
1551            "fn process<'a>(token: SimdToken, data: &'a [f32]) -> &'a [f32] { data }",
1552            "v3",
1553            false,
1554        );
1555        assert!(!f.sig.generics.params.is_empty());
1556    }
1557
1558    #[test]
1559    fn variant_replacement_scalar_self_injects_preamble() {
1560        let f = do_variant_replacement(
1561            "fn method(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1562            "scalar",
1563            true, // has_self
1564        );
1565        assert_eq!(f.sig.ident, "method_scalar");
1566
1567        // First statement should be `let _self = self;`
1568        let body_str = f.block.to_token_stream().to_string();
1569        assert!(
1570            body_str.contains("let _self = self"),
1571            "Scalar+self variant should have _self preamble, got: {}",
1572            body_str
1573        );
1574    }
1575
1576    #[test]
1577    fn variant_replacement_all_default_tiers_produce_valid_fns() {
1578        let names: Vec<String> = DEFAULT_TIER_NAMES.iter().map(|s| s.to_string()).collect();
1579        let tiers = resolve_tiers(&names, proc_macro2::Span::call_site(), false).unwrap();
1580
1581        for tier in &tiers {
1582            let f = do_variant_replacement(
1583                "fn process(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1584                tier.name,
1585                false,
1586            );
1587            let expected_name = format!("process_{}", tier.suffix);
1588            assert_eq!(
1589                f.sig.ident.to_string(),
1590                expected_name,
1591                "Tier '{}' should produce function '{}'",
1592                tier.name,
1593                expected_name
1594            );
1595        }
1596    }
1597
1598    #[test]
1599    fn variant_replacement_all_known_tiers_produce_valid_fns() {
1600        for tier in ALL_TIERS {
1601            let f = do_variant_replacement(
1602                "fn compute(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1603                tier.name,
1604                false,
1605            );
1606            let expected_name = format!("compute_{}", tier.suffix);
1607            assert_eq!(
1608                f.sig.ident.to_string(),
1609                expected_name,
1610                "Tier '{}' should produce function '{}'",
1611                tier.name,
1612                expected_name
1613            );
1614        }
1615    }
1616
1617    #[test]
1618    fn variant_replacement_no_simdtoken_remains() {
1619        for tier in ALL_TIERS {
1620            let f = do_variant_replacement(
1621                "fn compute(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1622                tier.name,
1623                false,
1624            );
1625            let full_str = f.to_token_stream().to_string();
1626            assert!(
1627                !full_str.contains("SimdToken"),
1628                "Tier '{}' variant still contains 'SimdToken': {}",
1629                tier.name,
1630                full_str
1631            );
1632        }
1633    }
1634
1635    // =========================================================================
1636    // autoversion — cfg guard and tier descriptor properties
1637    // =========================================================================
1638
1639    #[test]
1640    fn tier_v3_targets_x86_64() {
1641        let tier = find_tier("v3").unwrap();
1642        assert_eq!(tier.target_arch, Some("x86_64"));
1643    }
1644
1645    #[test]
1646    fn tier_v4_targets_x86_64() {
1647        let tier = find_tier("v4").unwrap();
1648        assert_eq!(tier.target_arch, Some("x86_64"));
1649    }
1650
1651    #[test]
1652    fn tier_v4x_targets_x86_64() {
1653        let tier = find_tier("v4x").unwrap();
1654        assert_eq!(tier.target_arch, Some("x86_64"));
1655    }
1656
1657    #[test]
1658    fn tier_neon_targets_aarch64() {
1659        let tier = find_tier("neon").unwrap();
1660        assert_eq!(tier.target_arch, Some("aarch64"));
1661    }
1662
1663    #[test]
1664    fn tier_wasm128_targets_wasm32() {
1665        let tier = find_tier("wasm128").unwrap();
1666        assert_eq!(tier.target_arch, Some("wasm32"));
1667    }
1668
1669    #[test]
1670    fn tier_scalar_has_no_guards() {
1671        let tier = find_tier("scalar").unwrap();
1672        assert_eq!(tier.target_arch, None);
1673        assert_eq!(tier.priority, 0);
1674    }
1675
1676    #[test]
1677    fn tier_priorities_are_consistent() {
1678        // Higher-capability tiers within the same arch should have higher priority
1679        let v2 = find_tier("v2").unwrap();
1680        let v3 = find_tier("v3").unwrap();
1681        let v4 = find_tier("v4").unwrap();
1682        assert!(v4.priority > v3.priority);
1683        assert!(v3.priority > v2.priority);
1684
1685        let neon = find_tier("neon").unwrap();
1686        let arm_v2 = find_tier("arm_v2").unwrap();
1687        let arm_v3 = find_tier("arm_v3").unwrap();
1688        assert!(arm_v3.priority > arm_v2.priority);
1689        assert!(arm_v2.priority > neon.priority);
1690
1691        // scalar is lowest
1692        let scalar = find_tier("scalar").unwrap();
1693        assert!(neon.priority > scalar.priority);
1694        assert!(v2.priority > scalar.priority);
1695    }
1696
1697    // =========================================================================
1698    // autoversion — dispatcher structure
1699    // =========================================================================
1700
1701    #[test]
1702    fn dispatcher_param_removal_free_fn() {
1703        // Simulate what autoversion_impl does: remove the SimdToken param
1704        let f: ItemFn =
1705            syn::parse_str("fn process(token: SimdToken, data: &[f32], scale: f32) -> f32 { 0.0 }")
1706                .unwrap();
1707
1708        let token_param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1709        // SimdToken → strip from dispatcher
1710        assert_eq!(token_param.kind, AutoversionTokenKind::SimdToken);
1711        let mut dispatcher_inputs: Vec<FnArg> = f.sig.inputs.iter().cloned().collect();
1712        dispatcher_inputs.remove(token_param.index);
1713        assert_eq!(dispatcher_inputs.len(), 2);
1714    }
1715
1716    #[test]
1717    fn dispatcher_param_removal_token_only() {
1718        let f: ItemFn = syn::parse_str("fn process(token: SimdToken) -> f32 { 0.0 }").unwrap();
1719        let token_param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1720        let mut dispatcher_inputs: Vec<FnArg> = f.sig.inputs.iter().cloned().collect();
1721        dispatcher_inputs.remove(token_param.index);
1722        assert_eq!(dispatcher_inputs.len(), 0);
1723    }
1724
1725    #[test]
1726    fn dispatcher_param_removal_token_last() {
1727        let f: ItemFn =
1728            syn::parse_str("fn process(data: &[f32], scale: f32, token: SimdToken) -> f32 { 0.0 }")
1729                .unwrap();
1730        let token_param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1731        assert_eq!(token_param.index, 2);
1732        let mut dispatcher_inputs: Vec<FnArg> = f.sig.inputs.iter().cloned().collect();
1733        dispatcher_inputs.remove(token_param.index);
1734        assert_eq!(dispatcher_inputs.len(), 2);
1735    }
1736
1737    #[test]
1738    fn dispatcher_scalar_token_kept() {
1739        // ScalarToken is a real type — kept in dispatcher for incant! compatibility
1740        let f: ItemFn =
1741            syn::parse_str("fn process_scalar(_: ScalarToken, data: &[f32]) -> f32 { 0.0 }")
1742                .unwrap();
1743        let token_param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1744        assert_eq!(token_param.kind, AutoversionTokenKind::ScalarToken);
1745        // Should NOT be removed — dispatcher keeps it
1746        assert_eq!(f.sig.inputs.len(), 2);
1747    }
1748
1749    #[test]
1750    fn dispatcher_dispatch_args_extraction() {
1751        // Test that we correctly extract idents for the dispatch call
1752        let f: ItemFn =
1753            syn::parse_str("fn process(data: &[f32], scale: f32) -> f32 { 0.0 }").unwrap();
1754
1755        let dispatch_args: Vec<String> = f
1756            .sig
1757            .inputs
1758            .iter()
1759            .filter_map(|arg| {
1760                if let FnArg::Typed(PatType { pat, .. }) = arg {
1761                    if let syn::Pat::Ident(pi) = pat.as_ref() {
1762                        return Some(pi.ident.to_string());
1763                    }
1764                }
1765                None
1766            })
1767            .collect();
1768
1769        assert_eq!(dispatch_args, vec!["data", "scale"]);
1770    }
1771
1772    #[test]
1773    fn dispatcher_wildcard_params_get_renamed() {
1774        let f: ItemFn = syn::parse_str("fn process(_: &[f32], _: f32) -> f32 { 0.0 }").unwrap();
1775
1776        let mut dispatcher_inputs: Vec<FnArg> = f.sig.inputs.iter().cloned().collect();
1777
1778        let mut wild_counter = 0u32;
1779        for arg in &mut dispatcher_inputs {
1780            if let FnArg::Typed(pat_type) = arg {
1781                if matches!(pat_type.pat.as_ref(), syn::Pat::Wild(_)) {
1782                    let ident = format_ident!("__autoversion_wild_{}", wild_counter);
1783                    wild_counter += 1;
1784                    *pat_type.pat = syn::Pat::Ident(syn::PatIdent {
1785                        attrs: vec![],
1786                        by_ref: None,
1787                        mutability: None,
1788                        ident,
1789                        subpat: None,
1790                    });
1791                }
1792            }
1793        }
1794
1795        // Both wildcards should be renamed
1796        assert_eq!(wild_counter, 2);
1797
1798        let names: Vec<String> = dispatcher_inputs
1799            .iter()
1800            .filter_map(|arg| {
1801                if let FnArg::Typed(PatType { pat, .. }) = arg {
1802                    if let syn::Pat::Ident(pi) = pat.as_ref() {
1803                        return Some(pi.ident.to_string());
1804                    }
1805                }
1806                None
1807            })
1808            .collect();
1809
1810        assert_eq!(names, vec!["__autoversion_wild_0", "__autoversion_wild_1"]);
1811    }
1812
1813    // =========================================================================
1814    // autoversion — suffix_path (reused in dispatch)
1815    // =========================================================================
1816
1817    #[test]
1818    fn suffix_path_simple() {
1819        let path: syn::Path = syn::parse_str("process").unwrap();
1820        let suffixed = suffix_path(&path, "v3");
1821        assert_eq!(suffixed.to_token_stream().to_string(), "process_v3");
1822    }
1823
1824    #[test]
1825    fn suffix_path_qualified() {
1826        let path: syn::Path = syn::parse_str("module::process").unwrap();
1827        let suffixed = suffix_path(&path, "neon");
1828        let s = suffixed.to_token_stream().to_string();
1829        assert!(
1830            s.contains("process_neon"),
1831            "Expected process_neon, got: {}",
1832            s
1833        );
1834    }
1835}