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