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 attribute args: [rite,] [define(type, ...),] tier1, tier2(feature), ...
405    //
406    // Special keywords:
407    //   `rite`: flag that changes per-tier variants to use
408    //           `#[archmage::rite(import_intrinsics)]` (direct
409    //           `#[target_feature]` + `#[inline]`) instead of
410    //           `#[archmage::arcane]` (safe wrapper + inner trampoline).
411    //
412    //   `define(name1, name2, ...)`: list of magetypes type names to inject
413    //           as local type aliases at the top of each variant body
414    //           (e.g., `type f32x8 = ::magetypes::simd::generic::f32x8<Token>;`).
415    //           `Token` in the alias RHS is substituted per tier.
416    //
417    // Assumption: neither `rite` nor `define` is or will become a tier name.
418    // `token-registry.toml` must not declare `short_name = "rite"` or
419    // `short_name = "define"`.
420    let (rite_flag, defines, tier_names) =
421        match syn::parse::Parser::parse(parse_magetypes_attr, attr) {
422            Ok(parsed) => parsed,
423            Err(e) => return e.to_compile_error().into(),
424        };
425
426    let tier_names = if tier_names.is_empty() {
427        DEFAULT_TIER_NAMES.iter().map(|s| s.to_string()).collect()
428    } else {
429        tier_names
430    };
431
432    // default_optional: tiers with cfg_feature are optional by default
433    let tiers = match resolve_tiers(
434        &tier_names,
435        input_fn.sig.ident.span(),
436        true, // magetypes always uses default_optional for cfg_feature tiers
437    ) {
438        Ok(t) => t,
439        Err(e) => return e.to_compile_error().into(),
440    };
441
442    magetypes_impl(input_fn, &tiers, rite_flag, &defines)
443}
444
445/// Parse `#[magetypes]` attributes: `rite` flag, `define(list)`, and tier names.
446///
447/// Returns `(rite_flag, defines, tier_names)`. Tier names preserve the
448/// `+`/`-` modifier prefixes and `(cfg(feat))` gates for the tier resolver.
449fn parse_magetypes_attr(
450    input: syn::parse::ParseStream,
451) -> syn::Result<(bool, Vec<String>, Vec<String>)> {
452    use syn::Token;
453    let mut rite_flag = false;
454    let mut defines = Vec::new();
455    let mut tier_names = Vec::new();
456
457    while !input.is_empty() {
458        // Peek the leading ident without consuming — `rite` and `define` are
459        // special, anything else is a tier name (possibly prefixed with +/-).
460        let peek_rite = input.peek(syn::Ident) && {
461            let fork = input.fork();
462            fork.parse::<syn::Ident>()
463                .is_ok_and(|i| i == "rite" && !fork.peek(syn::token::Paren))
464        };
465        let peek_define = input.peek(syn::Ident) && {
466            let fork = input.fork();
467            fork.parse::<syn::Ident>()
468                .is_ok_and(|i| i == "define" && fork.peek(syn::token::Paren))
469        };
470
471        if peek_rite {
472            let _: syn::Ident = input.parse()?;
473            rite_flag = true;
474        } else if peek_define {
475            let _: syn::Ident = input.parse()?;
476            let content;
477            syn::parenthesized!(content in input);
478            while !content.is_empty() {
479                let ty: syn::Ident = content.parse()?;
480                defines.push(ty.to_string());
481                if content.peek(Token![,]) {
482                    let _: Token![,] = content.parse()?;
483                }
484            }
485        } else {
486            // Fall through to tier-name parsing (preserves +/- prefix and cfg gates).
487            tier_names.push(parse_one_tier(input)?);
488        }
489
490        if input.peek(Token![,]) {
491            let _: Token![,] = input.parse()?;
492        }
493    }
494
495    Ok((rite_flag, defines, tier_names))
496}
497
498// =============================================================================
499// incant! macro - dispatch to platform-specific variants
500// =============================================================================
501// incant! macro - dispatch to platform-specific variants
502// =============================================================================
503
504/// Dispatch to platform-specific SIMD variants.
505///
506/// # Entry Point Mode (no token yet)
507///
508/// Summons tokens and dispatches to the best available variant:
509///
510/// ```rust,ignore
511/// pub fn public_api(data: &[f32]) -> f32 {
512///     incant!(dot(Token, data))
513/// }
514/// ```
515///
516/// Expands to runtime feature detection + dispatch to `dot_v3`, `dot_v4`,
517/// `dot_neon`, `dot_wasm128`, or `dot_scalar`. The `Token` marker is
518/// replaced with the summoned token. Token can appear at any position
519/// to match the callee's signature:
520///
521/// ```rust,ignore
522/// incant!(process(Token, data), [v3, scalar])  // token-first
523/// incant!(process(data, Token), [v3, scalar])  // token-last
524/// ```
525///
526/// If `Token` is omitted, the token is prepended (backward compatible).
527///
528/// # Explicit Tiers
529///
530/// Specify which tiers to dispatch to:
531///
532/// ```rust,ignore
533/// pub fn api(data: &[f32]) -> f32 {
534///     incant!(process(Token, data), [v1, v3, neon, scalar])
535/// }
536/// ```
537///
538/// Always include `scalar` in explicit tier lists. Currently auto-appended
539/// if omitted; will become a compile error in v1.0. Tiers are automatically
540/// sorted by dispatch priority (highest first).
541///
542/// Known tiers: `v1`, `v2`, `v3`, `v4`, `v4x`, `neon`, `neon_aes`,
543/// `neon_sha3`, `neon_crc`, `wasm128`, `wasm128_relaxed`, `scalar`.
544///
545/// # Automatic Rewriting (inside tier macros)
546///
547/// When `incant!` appears inside an `#[arcane]`, `#[rite]`, or
548/// `#[autoversion]` function body, the outer macro **rewrites** it to
549/// a direct call at compile time — bypassing the runtime dispatcher:
550///
551/// ```rust,ignore
552/// #[arcane]
553/// fn outer(token: X64V3Token, data: &[f32]) -> f32 {
554///     // Rewritten to: inner_v3(token, data) — zero overhead
555///     incant!(inner(token, data), [v3, scalar])
556/// }
557/// ```
558///
559/// The rewriter recognizes the caller's token variable by name and
560/// handles downcasting (V4 caller → V3 callee), upgrade attempts
561/// (summon a higher tier), and feature-gated tiers automatically.
562///
563/// Use `Token` or the caller's token variable name in the args to
564/// control token position:
565///
566/// ```rust,ignore
567/// #[arcane]
568/// fn outer(my_token: X64V3Token, data: &[f32]) -> f32 {
569///     // my_token recognized, placed where it appears in args
570///     incant!(inner(data, my_token), [v3, scalar])
571/// }
572/// ```
573///
574/// # Passthrough Mode (generic token dispatch)
575///
576/// For functions generic over token types, use `with token` for
577/// compile-time dispatch via `IntoConcreteToken`:
578///
579/// ```rust,ignore
580/// fn dispatch<T: IntoConcreteToken>(token: T, data: &[f32]) -> f32 {
581///     incant!(process(data) with token, [v3, neon, scalar])
582/// }
583/// ```
584///
585/// The compiler monomorphizes the dispatch — when `T = X64V3Token`,
586/// only the V3 branch survives. No runtime summon, no overhead.
587///
588/// This is different from the rewriter: passthrough works on generic
589/// `IntoConcreteToken` bounds where the concrete tier isn't known at
590/// macro time. The rewriter works when the concrete tier IS known
591/// (inside `#[arcane]`/`#[rite]`/`#[autoversion]` bodies).
592///
593/// # Variant Naming
594///
595/// Functions must have suffixed variants matching the selected tiers:
596/// - `_v1` for `X64V1Token`
597/// - `_v2` for `X64V2Token`
598/// - `_v3` for `X64V3Token`
599/// - `_v4` for `X64V4Token` (requires `avx512` feature)
600/// - `_v4x` for `X64V4xToken` (requires `avx512` feature)
601/// - `_neon` for `NeonToken`
602/// - `_neon_aes` for `NeonAesToken`
603/// - `_neon_sha3` for `NeonSha3Token`
604/// - `_neon_crc` for `NeonCrcToken`
605/// - `_wasm128` for `Wasm128Token`
606/// - `_scalar` for `ScalarToken`
607#[proc_macro]
608pub fn incant(input: TokenStream) -> TokenStream {
609    let input = parse_macro_input!(input as IncantInput);
610    incant_impl(input)
611}
612
613/// Legacy alias for [`incant!`].
614#[proc_macro]
615pub fn simd_route(input: TokenStream) -> TokenStream {
616    let input = parse_macro_input!(input as IncantInput);
617    incant_impl(input)
618}
619
620/// Descriptive alias for [`incant!`].
621///
622/// Dispatches to architecture-specific function variants at runtime.
623/// Looks for suffixed functions (`_v3`, `_v4`, `_neon`, `_wasm128`, `_scalar`)
624/// and calls the best one the CPU supports.
625///
626/// See [`incant!`] for full documentation and examples.
627#[proc_macro]
628pub fn dispatch_variant(input: TokenStream) -> TokenStream {
629    let input = parse_macro_input!(input as IncantInput);
630    incant_impl(input)
631}
632
633// =============================================================================
634
635/// Let the compiler auto-vectorize scalar code for each architecture.
636///
637/// Write a plain scalar function and let `#[autoversion]` generate
638/// architecture-specific copies — each compiled with different
639/// `#[target_feature]` flags via `#[arcane]` — plus a runtime dispatcher
640/// that calls the best one the CPU supports.
641///
642/// # Quick start
643///
644/// ```rust,ignore
645/// use archmage::autoversion;
646///
647/// #[autoversion]
648/// fn sum_of_squares(data: &[f32]) -> f32 {
649///     let mut sum = 0.0f32;
650///     for &x in data {
651///         sum += x * x;
652///     }
653///     sum
654/// }
655///
656/// // Call directly — no token, no unsafe:
657/// let result = sum_of_squares(&my_data);
658/// ```
659///
660/// Each variant gets `#[arcane]` → `#[target_feature(enable = "avx2,fma,...")]`,
661/// which unlocks the compiler's auto-vectorizer for that feature set.
662/// On x86-64, that loop compiles to `vfmadd231ps`. On aarch64, `fmla`.
663/// The `_scalar` fallback compiles without SIMD target features.
664///
665/// # SimdToken — optional placeholder
666///
667/// You can optionally write `_token: SimdToken` as a parameter. The macro
668/// recognizes it and strips it from the dispatcher — both forms produce
669/// identical output. Prefer the tokenless form for new code.
670///
671/// ```rust,ignore
672/// #[autoversion]
673/// fn normalize(_token: SimdToken, data: &mut [f32], scale: f32) {
674///     for x in data.iter_mut() { *x = (*x - 128.0) * scale; }
675/// }
676/// // Dispatcher is: fn normalize(data: &mut [f32], scale: f32)
677/// ```
678///
679/// # What gets generated
680///
681/// `#[autoversion] fn process(data: &[f32]) -> f32` expands to:
682///
683/// - `process_v4(token: X64V4Token, ...)` — AVX-512
684/// - `process_v3(token: X64V3Token, ...)` — AVX2+FMA
685/// - `process_neon(token: NeonToken, ...)` — aarch64 NEON
686/// - `process_wasm128(token: Wasm128Token, ...)` — WASM SIMD
687/// - `process_scalar(token: ScalarToken, ...)` — no SIMD, always available
688/// - `process(data: &[f32]) -> f32` — **dispatcher**
689///
690/// Variants are private. The dispatcher gets the original function's visibility.
691/// Within the same module, call variants directly for testing or benchmarking.
692///
693/// # Explicit tiers
694///
695/// ```rust,ignore
696/// #[autoversion(v3, v4, neon, arm_v2, wasm128)]
697/// fn process(data: &[f32]) -> f32 { ... }
698/// ```
699///
700/// `scalar` is always included implicitly.
701///
702/// Default tiers: `v4`, `v3`, `neon`, `wasm128`, `scalar`.
703///
704/// Known tiers: `v1`, `v2`, `v3`, `v3_crypto`, `v4`, `v4x`, `neon`,
705/// `neon_aes`, `neon_sha3`, `neon_crc`, `arm_v2`, `arm_v3`, `wasm128`,
706/// `wasm128_relaxed`, `x64_crypto`, `scalar`.
707///
708/// # Methods
709///
710/// For inherent methods, `self` works naturally:
711///
712/// ```rust,ignore
713/// impl ImageBuffer {
714///     #[autoversion]
715///     fn normalize(&mut self, gamma: f32) {
716///         for pixel in &mut self.data {
717///             *pixel = (*pixel / 255.0).powf(gamma);
718///         }
719///     }
720/// }
721/// buffer.normalize(2.2);
722/// ```
723///
724/// For trait method delegation, use `_self = Type` (nested mode):
725///
726/// ```rust,ignore
727/// impl MyType {
728///     #[autoversion(_self = MyType)]
729///     fn compute_impl(&self, data: &[f32]) -> f32 {
730///         _self.weights.iter().zip(data).map(|(w, d)| w * d).sum()
731///     }
732/// }
733/// ```
734///
735/// # Nesting with `incant!`
736///
737/// Hand-written SIMD for specific tiers, autoversion for the rest:
738///
739/// ```rust,ignore
740/// pub fn process(data: &[f32]) -> f32 {
741///     incant!(process(data), [v4, scalar])
742/// }
743///
744/// #[arcane(import_intrinsics)]
745/// fn process_v4(_t: X64V4Token, data: &[f32]) -> f32 { /* AVX-512 */ }
746///
747/// // Bridge: incant! passes ScalarToken, autoversion doesn't need one
748/// fn process_scalar(_: ScalarToken, data: &[f32]) -> f32 {
749///     process_auto(data)
750/// }
751///
752/// #[autoversion(v3, neon)]
753/// fn process_auto(data: &[f32]) -> f32 { data.iter().sum() }
754/// ```
755///
756/// # Comparison with `#[magetypes]` + `incant!`
757///
758/// | | `#[autoversion]` | `#[magetypes]` + `incant!` |
759/// |---|---|---|
760/// | Generates variants + dispatcher | Yes | Variants only (+ separate `incant!`) |
761/// | Body touched | No (signature only) | Yes (text substitution) |
762/// | Best for | Scalar auto-vectorization | Hand-written SIMD types |
763#[proc_macro_attribute]
764pub fn autoversion(attr: TokenStream, item: TokenStream) -> TokenStream {
765    let args = parse_macro_input!(attr as AutoversionArgs);
766    let input_fn = parse_macro_input!(item as LightFn);
767    autoversion_impl(input_fn, args)
768}
769
770// =============================================================================
771// Unit tests for token/trait recognition maps
772// =============================================================================
773
774#[cfg(test)]
775mod tests {
776    use super::*;
777
778    use super::generated::{ALL_CONCRETE_TOKENS, ALL_TRAIT_NAMES};
779    use syn::{ItemFn, ReturnType};
780
781    #[test]
782    fn every_concrete_token_is_in_token_to_features() {
783        for &name in ALL_CONCRETE_TOKENS {
784            assert!(
785                token_to_features(name).is_some(),
786                "Token `{}` exists in runtime crate but is NOT recognized by \
787                 token_to_features() in the proc macro. Add it!",
788                name
789            );
790        }
791    }
792
793    #[test]
794    fn every_trait_is_in_trait_to_features() {
795        for &name in ALL_TRAIT_NAMES {
796            assert!(
797                trait_to_features(name).is_some(),
798                "Trait `{}` exists in runtime crate but is NOT recognized by \
799                 trait_to_features() in the proc macro. Add it!",
800                name
801            );
802        }
803    }
804
805    #[test]
806    fn token_aliases_map_to_same_features() {
807        // Desktop64 = X64V3Token
808        assert_eq!(
809            token_to_features("Desktop64"),
810            token_to_features("X64V3Token"),
811            "Desktop64 and X64V3Token should map to identical features"
812        );
813
814        // Server64 = X64V4Token = Avx512Token
815        assert_eq!(
816            token_to_features("Server64"),
817            token_to_features("X64V4Token"),
818            "Server64 and X64V4Token should map to identical features"
819        );
820        assert_eq!(
821            token_to_features("X64V4Token"),
822            token_to_features("Avx512Token"),
823            "X64V4Token and Avx512Token should map to identical features"
824        );
825
826        // Arm64 = NeonToken
827        assert_eq!(
828            token_to_features("Arm64"),
829            token_to_features("NeonToken"),
830            "Arm64 and NeonToken should map to identical features"
831        );
832    }
833
834    #[test]
835    fn trait_to_features_includes_tokens_as_bounds() {
836        // Tier tokens should also work as trait bounds
837        // (for `impl X64V3Token` patterns, even though Rust won't allow it,
838        // the macro processes AST before type checking)
839        let tier_tokens = [
840            "X64V2Token",
841            "X64CryptoToken",
842            "X64V3Token",
843            "Desktop64",
844            "Avx2FmaToken",
845            "X64V4Token",
846            "Avx512Token",
847            "Server64",
848            "X64V4xToken",
849            "Avx512Fp16Token",
850            "NeonToken",
851            "Arm64",
852            "NeonAesToken",
853            "NeonSha3Token",
854            "NeonCrcToken",
855            "Arm64V2Token",
856            "Arm64V3Token",
857        ];
858
859        for &name in &tier_tokens {
860            assert!(
861                trait_to_features(name).is_some(),
862                "Tier token `{}` should also be recognized in trait_to_features() \
863                 for use as a generic bound. Add it!",
864                name
865            );
866        }
867    }
868
869    #[test]
870    fn trait_features_are_cumulative() {
871        // HasX64V4 should include all HasX64V2 features plus more
872        let v2_features = trait_to_features("HasX64V2").unwrap();
873        let v4_features = trait_to_features("HasX64V4").unwrap();
874
875        for &f in v2_features {
876            assert!(
877                v4_features.contains(&f),
878                "HasX64V4 should include v2 feature `{}` but doesn't",
879                f
880            );
881        }
882
883        // v4 should have more features than v2
884        assert!(
885            v4_features.len() > v2_features.len(),
886            "HasX64V4 should have more features than HasX64V2"
887        );
888    }
889
890    #[test]
891    fn x64v3_trait_features_include_v2() {
892        // X64V3Token as trait bound should include v2 features
893        let v2 = trait_to_features("HasX64V2").unwrap();
894        let v3 = trait_to_features("X64V3Token").unwrap();
895
896        for &f in v2 {
897            assert!(
898                v3.contains(&f),
899                "X64V3Token trait features should include v2 feature `{}` but don't",
900                f
901            );
902        }
903    }
904
905    #[test]
906    fn has_neon_aes_includes_neon() {
907        let neon = trait_to_features("HasNeon").unwrap();
908        let neon_aes = trait_to_features("HasNeonAes").unwrap();
909
910        for &f in neon {
911            assert!(
912                neon_aes.contains(&f),
913                "HasNeonAes should include NEON feature `{}`",
914                f
915            );
916        }
917    }
918
919    #[test]
920    fn no_removed_traits_are_recognized() {
921        // These traits were removed in 0.3.0 and should NOT be recognized
922        let removed = [
923            "HasSse",
924            "HasSse2",
925            "HasSse41",
926            "HasSse42",
927            "HasAvx",
928            "HasAvx2",
929            "HasFma",
930            "HasAvx512f",
931            "HasAvx512bw",
932            "HasAvx512vl",
933            "HasAvx512vbmi2",
934            "HasSve",
935            "HasSve2",
936        ];
937
938        for &name in &removed {
939            assert!(
940                trait_to_features(name).is_none(),
941                "Removed trait `{}` should NOT be in trait_to_features(). \
942                 It was removed in 0.3.0 — users should migrate to tier traits.",
943                name
944            );
945        }
946    }
947
948    #[test]
949    fn no_nonexistent_tokens_are_recognized() {
950        // These tokens don't exist and should NOT be recognized
951        let fake = [
952            "SveToken",
953            "Sve2Token",
954            "Avx512VnniToken",
955            "X64V4ModernToken",
956            "NeonFp16Token",
957        ];
958
959        for &name in &fake {
960            assert!(
961                token_to_features(name).is_none(),
962                "Non-existent token `{}` should NOT be in token_to_features()",
963                name
964            );
965        }
966    }
967
968    #[test]
969    fn featureless_traits_are_not_in_registries() {
970        // SimdToken and IntoConcreteToken should NOT be in any feature registry
971        // because they don't map to CPU features
972        for &name in FEATURELESS_TRAIT_NAMES {
973            assert!(
974                token_to_features(name).is_none(),
975                "`{}` should NOT be in token_to_features() — it has no CPU features",
976                name
977            );
978            assert!(
979                trait_to_features(name).is_none(),
980                "`{}` should NOT be in trait_to_features() — it has no CPU features",
981                name
982            );
983        }
984    }
985
986    #[test]
987    fn find_featureless_trait_detects_simdtoken() {
988        let names = vec!["SimdToken".to_string()];
989        assert_eq!(find_featureless_trait(&names), Some("SimdToken"));
990
991        let names = vec!["IntoConcreteToken".to_string()];
992        assert_eq!(find_featureless_trait(&names), Some("IntoConcreteToken"));
993
994        // Feature-bearing traits should NOT be detected
995        let names = vec!["HasX64V2".to_string()];
996        assert_eq!(find_featureless_trait(&names), None);
997
998        let names = vec!["HasNeon".to_string()];
999        assert_eq!(find_featureless_trait(&names), None);
1000
1001        // Mixed: if SimdToken is among real traits, still detected
1002        let names = vec!["SimdToken".to_string(), "HasX64V2".to_string()];
1003        assert_eq!(find_featureless_trait(&names), Some("SimdToken"));
1004    }
1005
1006    #[test]
1007    fn arm64_v2_v3_traits_are_cumulative() {
1008        let v2_features = trait_to_features("HasArm64V2").unwrap();
1009        let v3_features = trait_to_features("HasArm64V3").unwrap();
1010
1011        for &f in v2_features {
1012            assert!(
1013                v3_features.contains(&f),
1014                "HasArm64V3 should include v2 feature `{}` but doesn't",
1015                f
1016            );
1017        }
1018
1019        assert!(
1020            v3_features.len() > v2_features.len(),
1021            "HasArm64V3 should have more features than HasArm64V2"
1022        );
1023    }
1024
1025    // =========================================================================
1026    // resolve_tiers — additive / subtractive / override
1027    // =========================================================================
1028
1029    fn resolve_tier_names(names: &[&str], default_gates: bool) -> Vec<String> {
1030        let names: Vec<String> = names.iter().map(|s| s.to_string()).collect();
1031        resolve_tiers(&names, proc_macro2::Span::call_site(), default_gates)
1032            .unwrap()
1033            .iter()
1034            .map(|rt| {
1035                if let Some(ref gate) = rt.feature_gate {
1036                    format!("{}({})", rt.name, gate)
1037                } else {
1038                    rt.name.to_string()
1039                }
1040            })
1041            .collect()
1042    }
1043
1044    #[test]
1045    fn resolve_defaults() {
1046        let tiers = resolve_tier_names(&["v4", "v3", "neon", "wasm128", "scalar"], true);
1047        assert!(tiers.contains(&"v3".to_string()));
1048        assert!(tiers.contains(&"scalar".to_string()));
1049        // v4 gets auto-gated when default_feature_gates=true
1050        assert!(tiers.contains(&"v4(avx512)".to_string()));
1051    }
1052
1053    #[test]
1054    fn resolve_additive_appends() {
1055        let tiers = resolve_tier_names(&["+v1"], true);
1056        assert!(tiers.contains(&"v1".to_string()));
1057        assert!(tiers.contains(&"v3".to_string())); // from defaults
1058        assert!(tiers.contains(&"scalar".to_string())); // from defaults
1059    }
1060
1061    #[test]
1062    fn resolve_additive_v4_overrides_gate() {
1063        // +v4 should replace v4(avx512) with plain v4 (no gate)
1064        let tiers = resolve_tier_names(&["+v4"], true);
1065        assert!(tiers.contains(&"v4".to_string())); // no gate
1066        assert!(!tiers.iter().any(|t| t == "v4(avx512)")); // gated version gone
1067    }
1068
1069    #[test]
1070    fn resolve_additive_default_replaces_scalar() {
1071        let tiers = resolve_tier_names(&["+default"], true);
1072        assert!(tiers.contains(&"default".to_string()));
1073        assert!(!tiers.iter().any(|t| t == "scalar")); // scalar replaced
1074    }
1075
1076    #[test]
1077    fn resolve_subtractive_removes() {
1078        let tiers = resolve_tier_names(&["-neon", "-wasm128"], true);
1079        assert!(!tiers.iter().any(|t| t == "neon"));
1080        assert!(!tiers.iter().any(|t| t == "wasm128"));
1081        assert!(tiers.contains(&"v3".to_string())); // others remain
1082    }
1083
1084    #[test]
1085    fn resolve_subtractive_removes_scalar() {
1086        // -scalar must actually remove scalar — auto-append must not undo it
1087        let tiers = resolve_tier_names(&["-scalar"], true);
1088        assert!(
1089            !tiers.iter().any(|t| t == "scalar"),
1090            "expected scalar to be removed, got {tiers:?}"
1091        );
1092    }
1093
1094    #[test]
1095    fn resolve_subtractive_removes_scalar_with_other_modifiers() {
1096        // [-scalar, +arm_v2] — scalar removed, arm_v2 added, no scalar re-injected
1097        let tiers = resolve_tier_names(&["-scalar", "+arm_v2"], true);
1098        assert!(
1099            !tiers.iter().any(|t| t == "scalar"),
1100            "expected scalar to be removed, got {tiers:?}"
1101        );
1102        assert!(tiers.contains(&"arm_v2".to_string()));
1103    }
1104
1105    #[test]
1106    fn resolve_mixed_add_remove() {
1107        let tiers = resolve_tier_names(&["-neon", "-wasm128", "+v1"], true);
1108        assert!(tiers.contains(&"v1".to_string()));
1109        assert!(!tiers.iter().any(|t| t == "neon"));
1110        assert!(!tiers.iter().any(|t| t == "wasm128"));
1111        assert!(tiers.contains(&"v3".to_string()));
1112        assert!(tiers.contains(&"scalar".to_string()));
1113    }
1114
1115    #[test]
1116    fn resolve_additive_duplicate_is_noop() {
1117        // +v3 when v3 is already in defaults — no duplicate
1118        let tiers = resolve_tier_names(&["+v3"], true);
1119        let v3_count = tiers.iter().filter(|t| t.as_str() == "v3").count();
1120        assert_eq!(v3_count, 1);
1121    }
1122
1123    #[test]
1124    fn resolve_mixing_plus_and_plain_is_additive() {
1125        // #48: mixing `+tier` with plain `tier` is now allowed. Any `+` ⇒
1126        // additive mode; the plain tier is treated as `+tier`. Both v1 and v3
1127        // end up present (atop the defaults).
1128        let names: Vec<String> = vec!["+v1".into(), "v3".into()];
1129        let tiers = resolve_tiers(&names, proc_macro2::Span::call_site(), true).unwrap();
1130        let suffixes: Vec<&str> = tiers.iter().map(|t| t.tier.suffix).collect();
1131        assert!(suffixes.contains(&"v1"));
1132        assert!(suffixes.contains(&"v3"));
1133    }
1134
1135    #[test]
1136    fn resolve_underscore_tier_name() {
1137        let tiers = resolve_tier_names(&["_v3", "_neon", "_scalar"], false);
1138        assert!(tiers.contains(&"v3".to_string()));
1139        assert!(tiers.contains(&"neon".to_string()));
1140        assert!(tiers.contains(&"scalar".to_string()));
1141    }
1142
1143    // =========================================================================
1144    // autoversion — argument parsing
1145    // =========================================================================
1146
1147    #[test]
1148    fn autoversion_args_empty() {
1149        let args: AutoversionArgs = syn::parse_str("").unwrap();
1150        assert!(args.self_type.is_none());
1151        assert!(args.tiers.is_none());
1152    }
1153
1154    #[test]
1155    fn autoversion_args_single_tier() {
1156        let args: AutoversionArgs = syn::parse_str("v3").unwrap();
1157        assert!(args.self_type.is_none());
1158        assert_eq!(args.tiers.as_ref().unwrap(), &["v3"]);
1159    }
1160
1161    #[test]
1162    fn autoversion_args_tiers_only() {
1163        let args: AutoversionArgs = syn::parse_str("v3, v4, neon").unwrap();
1164        assert!(args.self_type.is_none());
1165        let tiers = args.tiers.unwrap();
1166        assert_eq!(tiers, vec!["v3", "v4", "neon"]);
1167    }
1168
1169    #[test]
1170    fn autoversion_args_many_tiers() {
1171        let args: AutoversionArgs =
1172            syn::parse_str("v1, v2, v3, v4, v4x, neon, arm_v2, wasm128").unwrap();
1173        assert_eq!(
1174            args.tiers.unwrap(),
1175            vec!["v1", "v2", "v3", "v4", "v4x", "neon", "arm_v2", "wasm128"]
1176        );
1177    }
1178
1179    #[test]
1180    fn autoversion_args_trailing_comma() {
1181        let args: AutoversionArgs = syn::parse_str("v3, v4,").unwrap();
1182        assert_eq!(args.tiers.as_ref().unwrap(), &["v3", "v4"]);
1183    }
1184
1185    #[test]
1186    fn autoversion_args_self_only() {
1187        let args: AutoversionArgs = syn::parse_str("_self = MyType").unwrap();
1188        assert!(args.self_type.is_some());
1189        assert!(args.tiers.is_none());
1190    }
1191
1192    #[test]
1193    fn autoversion_args_self_and_tiers() {
1194        let args: AutoversionArgs = syn::parse_str("_self = MyType, v3, neon").unwrap();
1195        assert!(args.self_type.is_some());
1196        let tiers = args.tiers.unwrap();
1197        assert_eq!(tiers, vec!["v3", "neon"]);
1198    }
1199
1200    #[test]
1201    fn autoversion_args_tiers_then_self() {
1202        // _self can appear after tier names
1203        let args: AutoversionArgs = syn::parse_str("v3, neon, _self = MyType").unwrap();
1204        assert!(args.self_type.is_some());
1205        let tiers = args.tiers.unwrap();
1206        assert_eq!(tiers, vec!["v3", "neon"]);
1207    }
1208
1209    #[test]
1210    fn autoversion_args_self_with_path_type() {
1211        let args: AutoversionArgs = syn::parse_str("_self = crate::MyType").unwrap();
1212        assert!(args.self_type.is_some());
1213        assert!(args.tiers.is_none());
1214    }
1215
1216    #[test]
1217    fn autoversion_args_self_with_generic_type() {
1218        let args: AutoversionArgs = syn::parse_str("_self = Vec<u8>").unwrap();
1219        assert!(args.self_type.is_some());
1220        let ty_str = args.self_type.unwrap().to_token_stream().to_string();
1221        assert!(ty_str.contains("Vec"), "Expected Vec<u8>, got: {}", ty_str);
1222    }
1223
1224    #[test]
1225    fn autoversion_args_self_trailing_comma() {
1226        let args: AutoversionArgs = syn::parse_str("_self = MyType,").unwrap();
1227        assert!(args.self_type.is_some());
1228        assert!(args.tiers.is_none());
1229    }
1230
1231    // =========================================================================
1232    // autoversion — find_autoversion_token_param
1233    // =========================================================================
1234
1235    #[test]
1236    fn find_autoversion_token_param_simdtoken_first() {
1237        let f: ItemFn =
1238            syn::parse_str("fn process(token: SimdToken, data: &[f32]) -> f32 {}").unwrap();
1239        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1240        assert_eq!(param.index, 0);
1241        assert_eq!(param.ident, "token");
1242        assert_eq!(param.kind, AutoversionTokenKind::SimdToken);
1243    }
1244
1245    #[test]
1246    fn find_autoversion_token_param_simdtoken_second() {
1247        let f: ItemFn =
1248            syn::parse_str("fn process(data: &[f32], token: SimdToken) -> f32 {}").unwrap();
1249        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1250        assert_eq!(param.index, 1);
1251        assert_eq!(param.kind, AutoversionTokenKind::SimdToken);
1252    }
1253
1254    #[test]
1255    fn find_autoversion_token_param_underscore_prefix() {
1256        let f: ItemFn =
1257            syn::parse_str("fn process(_token: SimdToken, data: &[f32]) -> f32 {}").unwrap();
1258        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1259        assert_eq!(param.index, 0);
1260        assert_eq!(param.ident, "_token");
1261    }
1262
1263    #[test]
1264    fn find_autoversion_token_param_wildcard() {
1265        let f: ItemFn = syn::parse_str("fn process(_: SimdToken, data: &[f32]) -> f32 {}").unwrap();
1266        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1267        assert_eq!(param.index, 0);
1268        assert_eq!(param.ident, "__autoversion_token");
1269    }
1270
1271    #[test]
1272    fn find_autoversion_token_param_scalar_token() {
1273        let f: ItemFn =
1274            syn::parse_str("fn process_scalar(_: ScalarToken, data: &[f32]) -> f32 {}").unwrap();
1275        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1276        assert_eq!(param.index, 0);
1277        assert_eq!(param.kind, AutoversionTokenKind::ScalarToken);
1278    }
1279
1280    #[test]
1281    fn find_autoversion_token_param_not_found() {
1282        let f: ItemFn = syn::parse_str("fn process(data: &[f32]) -> f32 {}").unwrap();
1283        assert!(find_autoversion_token_param(&f.sig).unwrap().is_none());
1284    }
1285
1286    #[test]
1287    fn find_autoversion_token_param_no_params() {
1288        let f: ItemFn = syn::parse_str("fn process() {}").unwrap();
1289        assert!(find_autoversion_token_param(&f.sig).unwrap().is_none());
1290    }
1291
1292    #[test]
1293    fn find_autoversion_token_param_concrete_token_errors() {
1294        let f: ItemFn =
1295            syn::parse_str("fn process(token: X64V3Token, data: &[f32]) -> f32 {}").unwrap();
1296        let err = find_autoversion_token_param(&f.sig).unwrap_err();
1297        let msg = err.to_string();
1298        assert!(
1299            msg.contains("concrete token"),
1300            "error should mention concrete token: {msg}"
1301        );
1302        assert!(
1303            msg.contains("#[arcane]"),
1304            "error should suggest #[arcane]: {msg}"
1305        );
1306    }
1307
1308    #[test]
1309    fn find_autoversion_token_param_neon_token_errors() {
1310        let f: ItemFn =
1311            syn::parse_str("fn process(token: NeonToken, data: &[f32]) -> f32 {}").unwrap();
1312        assert!(find_autoversion_token_param(&f.sig).is_err());
1313    }
1314
1315    #[test]
1316    fn find_autoversion_token_param_unknown_type_ignored() {
1317        // Random types are just regular params, not token params
1318        let f: ItemFn = syn::parse_str("fn process(data: &[f32], scale: f32) -> f32 {}").unwrap();
1319        assert!(find_autoversion_token_param(&f.sig).unwrap().is_none());
1320    }
1321
1322    #[test]
1323    fn find_autoversion_token_param_among_many() {
1324        let f: ItemFn = syn::parse_str(
1325            "fn process(a: i32, b: f64, token: SimdToken, c: &str, d: bool) -> f32 {}",
1326        )
1327        .unwrap();
1328        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1329        assert_eq!(param.index, 2);
1330        assert_eq!(param.ident, "token");
1331    }
1332
1333    #[test]
1334    fn find_autoversion_token_param_with_generics() {
1335        let f: ItemFn =
1336            syn::parse_str("fn process<T: Clone>(token: SimdToken, data: &[T]) -> T {}").unwrap();
1337        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1338        assert_eq!(param.index, 0);
1339    }
1340
1341    #[test]
1342    fn find_autoversion_token_param_with_where_clause() {
1343        let f: ItemFn = syn::parse_str(
1344            "fn process<T>(token: SimdToken, data: &[T]) -> T where T: Copy + Default {}",
1345        )
1346        .unwrap();
1347        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1348        assert_eq!(param.index, 0);
1349    }
1350
1351    #[test]
1352    fn find_autoversion_token_param_with_lifetime() {
1353        let f: ItemFn =
1354            syn::parse_str("fn process<'a>(token: SimdToken, data: &'a [f32]) -> &'a f32 {}")
1355                .unwrap();
1356        let param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1357        assert_eq!(param.index, 0);
1358    }
1359
1360    // =========================================================================
1361    // autoversion — tier resolution
1362    // =========================================================================
1363
1364    #[test]
1365    fn autoversion_default_tiers_all_resolve() {
1366        let names: Vec<String> = DEFAULT_TIER_NAMES.iter().map(|s| s.to_string()).collect();
1367        let tiers = resolve_tiers(&names, proc_macro2::Span::call_site(), false).unwrap();
1368        assert!(!tiers.is_empty());
1369        // scalar should be present
1370        assert!(tiers.iter().any(|t| t.name == "scalar"));
1371    }
1372
1373    #[test]
1374    fn autoversion_scalar_always_appended() {
1375        let names = vec!["v3".to_string(), "neon".to_string()];
1376        let tiers = resolve_tiers(&names, proc_macro2::Span::call_site(), false).unwrap();
1377        assert!(
1378            tiers.iter().any(|t| t.name == "scalar"),
1379            "scalar must be auto-appended"
1380        );
1381    }
1382
1383    #[test]
1384    fn autoversion_scalar_not_duplicated() {
1385        let names = vec!["v3".to_string(), "scalar".to_string()];
1386        let tiers = resolve_tiers(&names, proc_macro2::Span::call_site(), false).unwrap();
1387        let scalar_count = tiers.iter().filter(|t| t.name == "scalar").count();
1388        assert_eq!(scalar_count, 1, "scalar must not be duplicated");
1389    }
1390
1391    #[test]
1392    fn autoversion_tiers_sorted_by_priority() {
1393        let names = vec!["neon".to_string(), "v4".to_string(), "v3".to_string()];
1394        let tiers = resolve_tiers(&names, proc_macro2::Span::call_site(), false).unwrap();
1395        // v4 (priority 40) > v3 (30) > neon (20) > scalar (0)
1396        let priorities: Vec<u32> = tiers.iter().map(|t| t.priority).collect();
1397        for window in priorities.windows(2) {
1398            assert!(
1399                window[0] >= window[1],
1400                "Tiers not sorted by priority: {:?}",
1401                priorities
1402            );
1403        }
1404    }
1405
1406    #[test]
1407    fn autoversion_unknown_tier_errors() {
1408        let names = vec!["v3".to_string(), "avx9000".to_string()];
1409        let result = resolve_tiers(&names, proc_macro2::Span::call_site(), false);
1410        match result {
1411            Ok(_) => panic!("Expected error for unknown tier 'avx9000'"),
1412            Err(e) => {
1413                let err_msg = e.to_string();
1414                assert!(
1415                    err_msg.contains("avx9000"),
1416                    "Error should mention unknown tier: {}",
1417                    err_msg
1418                );
1419            }
1420        }
1421    }
1422
1423    #[test]
1424    fn autoversion_all_known_tiers_resolve() {
1425        // Every tier in ALL_TIERS should be findable
1426        for tier in ALL_TIERS {
1427            assert!(
1428                find_tier(tier.name).is_some(),
1429                "Tier '{}' should be findable by name",
1430                tier.name
1431            );
1432        }
1433    }
1434
1435    #[test]
1436    fn autoversion_default_tier_list_is_sensible() {
1437        // Defaults should cover x86, ARM, WASM, and scalar
1438        let names: Vec<String> = DEFAULT_TIER_NAMES.iter().map(|s| s.to_string()).collect();
1439        let tiers = resolve_tiers(&names, proc_macro2::Span::call_site(), false).unwrap();
1440
1441        let has_x86 = tiers.iter().any(|t| t.target_arch == Some("x86_64"));
1442        let has_arm = tiers.iter().any(|t| t.target_arch == Some("aarch64"));
1443        let has_wasm = tiers.iter().any(|t| t.target_arch == Some("wasm32"));
1444        let has_scalar = tiers.iter().any(|t| t.name == "scalar");
1445
1446        assert!(has_x86, "Default tiers should include an x86_64 tier");
1447        assert!(has_arm, "Default tiers should include an aarch64 tier");
1448        assert!(has_wasm, "Default tiers should include a wasm32 tier");
1449        assert!(has_scalar, "Default tiers should include scalar");
1450    }
1451
1452    // =========================================================================
1453    // autoversion — variant replacement (AST manipulation)
1454    // =========================================================================
1455
1456    /// Mirrors what `autoversion_impl` does for a single variant: parse an
1457    /// ItemFn (for test convenience), rename it, swap the SimdToken param
1458    /// type, optionally inject the `_self` preamble for scalar+self.
1459    fn do_variant_replacement(func: &str, tier_name: &str, has_self: bool) -> ItemFn {
1460        let mut f: ItemFn = syn::parse_str(func).unwrap();
1461        let fn_name = f.sig.ident.to_string();
1462
1463        let tier = find_tier(tier_name).unwrap();
1464
1465        // Rename
1466        f.sig.ident = format_ident!("{}_{}", fn_name, tier.suffix);
1467
1468        // Find and replace SimdToken param type (skip for "default" — tokenless)
1469        let token_idx = find_autoversion_token_param(&f.sig)
1470            .expect("should not error on SimdToken")
1471            .unwrap_or_else(|| panic!("No SimdToken param in: {}", func))
1472            .index;
1473        if tier_name == "default" {
1474            // Remove the token param for default tier
1475            let stmts = f.block.stmts.clone();
1476            let mut inputs: Vec<FnArg> = f.sig.inputs.iter().cloned().collect();
1477            inputs.remove(token_idx);
1478            f.sig.inputs = inputs.into_iter().collect();
1479            f.block.stmts = stmts;
1480        } else {
1481            let concrete_type: Type = syn::parse_str(tier.token_path).unwrap();
1482            if let FnArg::Typed(pt) = &mut f.sig.inputs[token_idx] {
1483                *pt.ty = concrete_type;
1484            }
1485        }
1486
1487        // Fallback (scalar/default) + self: inject preamble
1488        if (tier_name == "scalar" || tier_name == "default") && has_self {
1489            let preamble: syn::Stmt = syn::parse_quote!(let _self = self;);
1490            f.block.stmts.insert(0, preamble);
1491        }
1492
1493        f
1494    }
1495
1496    #[test]
1497    fn variant_replacement_v3_renames_function() {
1498        let f = do_variant_replacement(
1499            "fn process(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1500            "v3",
1501            false,
1502        );
1503        assert_eq!(f.sig.ident, "process_v3");
1504    }
1505
1506    #[test]
1507    fn variant_replacement_v3_replaces_token_type() {
1508        let f = do_variant_replacement(
1509            "fn process(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1510            "v3",
1511            false,
1512        );
1513        let first_param_ty = match &f.sig.inputs[0] {
1514            FnArg::Typed(pt) => pt.ty.to_token_stream().to_string(),
1515            _ => panic!("Expected typed param"),
1516        };
1517        assert!(
1518            first_param_ty.contains("X64V3Token"),
1519            "Expected X64V3Token, got: {}",
1520            first_param_ty
1521        );
1522    }
1523
1524    #[test]
1525    fn variant_replacement_neon_produces_valid_fn() {
1526        let f = do_variant_replacement(
1527            "fn compute(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1528            "neon",
1529            false,
1530        );
1531        assert_eq!(f.sig.ident, "compute_neon");
1532        let first_param_ty = match &f.sig.inputs[0] {
1533            FnArg::Typed(pt) => pt.ty.to_token_stream().to_string(),
1534            _ => panic!("Expected typed param"),
1535        };
1536        assert!(
1537            first_param_ty.contains("NeonToken"),
1538            "Expected NeonToken, got: {}",
1539            first_param_ty
1540        );
1541    }
1542
1543    #[test]
1544    fn variant_replacement_wasm128_produces_valid_fn() {
1545        let f = do_variant_replacement(
1546            "fn compute(_t: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1547            "wasm128",
1548            false,
1549        );
1550        assert_eq!(f.sig.ident, "compute_wasm128");
1551    }
1552
1553    #[test]
1554    fn variant_replacement_scalar_produces_valid_fn() {
1555        let f = do_variant_replacement(
1556            "fn compute(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1557            "scalar",
1558            false,
1559        );
1560        assert_eq!(f.sig.ident, "compute_scalar");
1561        let first_param_ty = match &f.sig.inputs[0] {
1562            FnArg::Typed(pt) => pt.ty.to_token_stream().to_string(),
1563            _ => panic!("Expected typed param"),
1564        };
1565        assert!(
1566            first_param_ty.contains("ScalarToken"),
1567            "Expected ScalarToken, got: {}",
1568            first_param_ty
1569        );
1570    }
1571
1572    #[test]
1573    fn variant_replacement_v4_produces_valid_fn() {
1574        let f = do_variant_replacement(
1575            "fn transform(token: SimdToken, data: &mut [f32]) { }",
1576            "v4",
1577            false,
1578        );
1579        assert_eq!(f.sig.ident, "transform_v4");
1580        let first_param_ty = match &f.sig.inputs[0] {
1581            FnArg::Typed(pt) => pt.ty.to_token_stream().to_string(),
1582            _ => panic!("Expected typed param"),
1583        };
1584        assert!(
1585            first_param_ty.contains("X64V4Token"),
1586            "Expected X64V4Token, got: {}",
1587            first_param_ty
1588        );
1589    }
1590
1591    #[test]
1592    fn variant_replacement_v4x_produces_valid_fn() {
1593        let f = do_variant_replacement(
1594            "fn transform(token: SimdToken, data: &mut [f32]) { }",
1595            "v4x",
1596            false,
1597        );
1598        assert_eq!(f.sig.ident, "transform_v4x");
1599    }
1600
1601    #[test]
1602    fn variant_replacement_arm_v2_produces_valid_fn() {
1603        let f = do_variant_replacement(
1604            "fn transform(token: SimdToken, data: &mut [f32]) { }",
1605            "arm_v2",
1606            false,
1607        );
1608        assert_eq!(f.sig.ident, "transform_arm_v2");
1609    }
1610
1611    #[test]
1612    fn variant_replacement_preserves_generics() {
1613        let f = do_variant_replacement(
1614            "fn process<T: Copy + Default>(token: SimdToken, data: &[T]) -> T { T::default() }",
1615            "v3",
1616            false,
1617        );
1618        assert_eq!(f.sig.ident, "process_v3");
1619        // Generic params should still be present
1620        assert!(
1621            !f.sig.generics.params.is_empty(),
1622            "Generics should be preserved"
1623        );
1624    }
1625
1626    #[test]
1627    fn variant_replacement_preserves_where_clause() {
1628        let f = do_variant_replacement(
1629            "fn process<T>(token: SimdToken, data: &[T]) -> T where T: Copy + Default { T::default() }",
1630            "v3",
1631            false,
1632        );
1633        assert!(
1634            f.sig.generics.where_clause.is_some(),
1635            "Where clause should be preserved"
1636        );
1637    }
1638
1639    #[test]
1640    fn variant_replacement_preserves_return_type() {
1641        let f = do_variant_replacement(
1642            "fn process(token: SimdToken, data: &[f32]) -> Vec<f32> { vec![] }",
1643            "neon",
1644            false,
1645        );
1646        let ret = f.sig.output.to_token_stream().to_string();
1647        assert!(
1648            ret.contains("Vec"),
1649            "Return type should be preserved, got: {}",
1650            ret
1651        );
1652    }
1653
1654    #[test]
1655    fn variant_replacement_preserves_multiple_params() {
1656        let f = do_variant_replacement(
1657            "fn process(token: SimdToken, a: &[f32], b: &[f32], scale: f32) -> f32 { 0.0 }",
1658            "v3",
1659            false,
1660        );
1661        // SimdToken → X64V3Token, plus the 3 other params
1662        assert_eq!(f.sig.inputs.len(), 4);
1663    }
1664
1665    #[test]
1666    fn variant_replacement_preserves_no_return_type() {
1667        let f = do_variant_replacement(
1668            "fn transform(token: SimdToken, data: &mut [f32]) { }",
1669            "v3",
1670            false,
1671        );
1672        assert!(
1673            matches!(f.sig.output, ReturnType::Default),
1674            "No return type should remain as Default"
1675        );
1676    }
1677
1678    #[test]
1679    fn variant_replacement_preserves_lifetime_params() {
1680        let f = do_variant_replacement(
1681            "fn process<'a>(token: SimdToken, data: &'a [f32]) -> &'a [f32] { data }",
1682            "v3",
1683            false,
1684        );
1685        assert!(!f.sig.generics.params.is_empty());
1686    }
1687
1688    #[test]
1689    fn variant_replacement_scalar_self_injects_preamble() {
1690        let f = do_variant_replacement(
1691            "fn method(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1692            "scalar",
1693            true, // has_self
1694        );
1695        assert_eq!(f.sig.ident, "method_scalar");
1696
1697        // First statement should be `let _self = self;`
1698        let body_str = f.block.to_token_stream().to_string();
1699        assert!(
1700            body_str.contains("let _self = self"),
1701            "Scalar+self variant should have _self preamble, got: {}",
1702            body_str
1703        );
1704    }
1705
1706    #[test]
1707    fn variant_replacement_all_default_tiers_produce_valid_fns() {
1708        let names: Vec<String> = DEFAULT_TIER_NAMES.iter().map(|s| s.to_string()).collect();
1709        let tiers = resolve_tiers(&names, proc_macro2::Span::call_site(), false).unwrap();
1710
1711        for tier in &tiers {
1712            let f = do_variant_replacement(
1713                "fn process(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1714                tier.name,
1715                false,
1716            );
1717            let expected_name = format!("process_{}", tier.suffix);
1718            assert_eq!(
1719                f.sig.ident.to_string(),
1720                expected_name,
1721                "Tier '{}' should produce function '{}'",
1722                tier.name,
1723                expected_name
1724            );
1725        }
1726    }
1727
1728    #[test]
1729    fn variant_replacement_all_known_tiers_produce_valid_fns() {
1730        for tier in ALL_TIERS {
1731            let f = do_variant_replacement(
1732                "fn compute(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1733                tier.name,
1734                false,
1735            );
1736            let expected_name = format!("compute_{}", tier.suffix);
1737            assert_eq!(
1738                f.sig.ident.to_string(),
1739                expected_name,
1740                "Tier '{}' should produce function '{}'",
1741                tier.name,
1742                expected_name
1743            );
1744        }
1745    }
1746
1747    #[test]
1748    fn variant_replacement_no_simdtoken_remains() {
1749        for tier in ALL_TIERS {
1750            let f = do_variant_replacement(
1751                "fn compute(token: SimdToken, data: &[f32]) -> f32 { 0.0 }",
1752                tier.name,
1753                false,
1754            );
1755            let full_str = f.to_token_stream().to_string();
1756            assert!(
1757                !full_str.contains("SimdToken"),
1758                "Tier '{}' variant still contains 'SimdToken': {}",
1759                tier.name,
1760                full_str
1761            );
1762        }
1763    }
1764
1765    // =========================================================================
1766    // autoversion — cfg guard and tier descriptor properties
1767    // =========================================================================
1768
1769    #[test]
1770    fn tier_v3_targets_x86_64() {
1771        let tier = find_tier("v3").unwrap();
1772        assert_eq!(tier.target_arch, Some("x86_64"));
1773    }
1774
1775    #[test]
1776    fn tier_v4_targets_x86_64() {
1777        let tier = find_tier("v4").unwrap();
1778        assert_eq!(tier.target_arch, Some("x86_64"));
1779    }
1780
1781    #[test]
1782    fn tier_v4x_targets_x86_64() {
1783        let tier = find_tier("v4x").unwrap();
1784        assert_eq!(tier.target_arch, Some("x86_64"));
1785    }
1786
1787    #[test]
1788    fn tier_neon_targets_aarch64() {
1789        let tier = find_tier("neon").unwrap();
1790        assert_eq!(tier.target_arch, Some("aarch64"));
1791    }
1792
1793    #[test]
1794    fn tier_wasm128_targets_wasm32() {
1795        let tier = find_tier("wasm128").unwrap();
1796        assert_eq!(tier.target_arch, Some("wasm32"));
1797    }
1798
1799    #[test]
1800    fn tier_scalar_has_no_guards() {
1801        let tier = find_tier("scalar").unwrap();
1802        assert_eq!(tier.target_arch, None);
1803        assert_eq!(tier.priority, 0);
1804    }
1805
1806    #[test]
1807    fn tier_priorities_are_consistent() {
1808        // Higher-capability tiers within the same arch should have higher priority
1809        let v2 = find_tier("v2").unwrap();
1810        let v3 = find_tier("v3").unwrap();
1811        let v4 = find_tier("v4").unwrap();
1812        assert!(v4.priority > v3.priority);
1813        assert!(v3.priority > v2.priority);
1814
1815        let neon = find_tier("neon").unwrap();
1816        let arm_v2 = find_tier("arm_v2").unwrap();
1817        let arm_v3 = find_tier("arm_v3").unwrap();
1818        assert!(arm_v3.priority > arm_v2.priority);
1819        assert!(arm_v2.priority > neon.priority);
1820
1821        // scalar is lowest
1822        let scalar = find_tier("scalar").unwrap();
1823        assert!(neon.priority > scalar.priority);
1824        assert!(v2.priority > scalar.priority);
1825    }
1826
1827    // =========================================================================
1828    // autoversion — dispatcher structure
1829    // =========================================================================
1830
1831    #[test]
1832    fn dispatcher_param_removal_free_fn() {
1833        // Simulate what autoversion_impl does: remove the SimdToken param
1834        let f: ItemFn =
1835            syn::parse_str("fn process(token: SimdToken, data: &[f32], scale: f32) -> f32 { 0.0 }")
1836                .unwrap();
1837
1838        let token_param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1839        // SimdToken → strip from dispatcher
1840        assert_eq!(token_param.kind, AutoversionTokenKind::SimdToken);
1841        let mut dispatcher_inputs: Vec<FnArg> = f.sig.inputs.iter().cloned().collect();
1842        dispatcher_inputs.remove(token_param.index);
1843        assert_eq!(dispatcher_inputs.len(), 2);
1844    }
1845
1846    #[test]
1847    fn dispatcher_param_removal_token_only() {
1848        let f: ItemFn = syn::parse_str("fn process(token: SimdToken) -> f32 { 0.0 }").unwrap();
1849        let token_param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1850        let mut dispatcher_inputs: Vec<FnArg> = f.sig.inputs.iter().cloned().collect();
1851        dispatcher_inputs.remove(token_param.index);
1852        assert_eq!(dispatcher_inputs.len(), 0);
1853    }
1854
1855    #[test]
1856    fn dispatcher_param_removal_token_last() {
1857        let f: ItemFn =
1858            syn::parse_str("fn process(data: &[f32], scale: f32, token: SimdToken) -> f32 { 0.0 }")
1859                .unwrap();
1860        let token_param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1861        assert_eq!(token_param.index, 2);
1862        let mut dispatcher_inputs: Vec<FnArg> = f.sig.inputs.iter().cloned().collect();
1863        dispatcher_inputs.remove(token_param.index);
1864        assert_eq!(dispatcher_inputs.len(), 2);
1865    }
1866
1867    #[test]
1868    fn dispatcher_scalar_token_kept() {
1869        // ScalarToken is a real type — kept in dispatcher for incant! compatibility
1870        let f: ItemFn =
1871            syn::parse_str("fn process_scalar(_: ScalarToken, data: &[f32]) -> f32 { 0.0 }")
1872                .unwrap();
1873        let token_param = find_autoversion_token_param(&f.sig).unwrap().unwrap();
1874        assert_eq!(token_param.kind, AutoversionTokenKind::ScalarToken);
1875        // Should NOT be removed — dispatcher keeps it
1876        assert_eq!(f.sig.inputs.len(), 2);
1877    }
1878
1879    #[test]
1880    fn dispatcher_dispatch_args_extraction() {
1881        // Test that we correctly extract idents for the dispatch call
1882        let f: ItemFn =
1883            syn::parse_str("fn process(data: &[f32], scale: f32) -> f32 { 0.0 }").unwrap();
1884
1885        let dispatch_args: Vec<String> = f
1886            .sig
1887            .inputs
1888            .iter()
1889            .filter_map(|arg| {
1890                if let FnArg::Typed(PatType { pat, .. }) = arg {
1891                    if let syn::Pat::Ident(pi) = pat.as_ref() {
1892                        return Some(pi.ident.to_string());
1893                    }
1894                }
1895                None
1896            })
1897            .collect();
1898
1899        assert_eq!(dispatch_args, vec!["data", "scale"]);
1900    }
1901
1902    #[test]
1903    fn dispatcher_wildcard_params_get_renamed() {
1904        let f: ItemFn = syn::parse_str("fn process(_: &[f32], _: f32) -> f32 { 0.0 }").unwrap();
1905
1906        let mut dispatcher_inputs: Vec<FnArg> = f.sig.inputs.iter().cloned().collect();
1907
1908        let mut wild_counter = 0u32;
1909        for arg in &mut dispatcher_inputs {
1910            if let FnArg::Typed(pat_type) = arg {
1911                if matches!(pat_type.pat.as_ref(), syn::Pat::Wild(_)) {
1912                    let ident = format_ident!("__autoversion_wild_{}", wild_counter);
1913                    wild_counter += 1;
1914                    *pat_type.pat = syn::Pat::Ident(syn::PatIdent {
1915                        attrs: vec![],
1916                        by_ref: None,
1917                        mutability: None,
1918                        ident,
1919                        subpat: None,
1920                    });
1921                }
1922            }
1923        }
1924
1925        // Both wildcards should be renamed
1926        assert_eq!(wild_counter, 2);
1927
1928        let names: Vec<String> = dispatcher_inputs
1929            .iter()
1930            .filter_map(|arg| {
1931                if let FnArg::Typed(PatType { pat, .. }) = arg {
1932                    if let syn::Pat::Ident(pi) = pat.as_ref() {
1933                        return Some(pi.ident.to_string());
1934                    }
1935                }
1936                None
1937            })
1938            .collect();
1939
1940        assert_eq!(names, vec!["__autoversion_wild_0", "__autoversion_wild_1"]);
1941    }
1942
1943    // =========================================================================
1944    // autoversion — suffix_path (reused in dispatch)
1945    // =========================================================================
1946
1947    #[test]
1948    fn suffix_path_simple() {
1949        let path: syn::Path = syn::parse_str("process").unwrap();
1950        let suffixed = suffix_path(&path, "v3");
1951        assert_eq!(suffixed.to_token_stream().to_string(), "process_v3");
1952    }
1953
1954    #[test]
1955    fn suffix_path_qualified() {
1956        let path: syn::Path = syn::parse_str("module::process").unwrap();
1957        let suffixed = suffix_path(&path, "neon");
1958        let s = suffixed.to_token_stream().to_string();
1959        assert!(
1960            s.contains("process_neon"),
1961            "Expected process_neon, got: {}",
1962            s
1963        );
1964    }
1965}