Skip to main content

nexus_rt_derive/
lib.rs

1//! Derive macros for nexus-rt.
2//!
3//! Use `nexus-rt` instead of depending on this crate directly.
4//! The derives are re-exported from `nexus_rt::{Resource, Deref, DerefMut, select}`.
5
6mod select;
7
8use proc_macro::TokenStream;
9use quote::{format_ident, quote};
10use syn::visit_mut::VisitMut;
11use syn::{Data, DeriveInput, Fields, Lifetime, parse_macro_input};
12
13// =============================================================================
14// #[derive(Resource)]
15// =============================================================================
16
17/// Derive the `Resource` marker trait, allowing this type to be stored
18/// in a `World`.
19///
20/// ```ignore
21/// use nexus_rt::Resource;
22///
23/// #[derive(Resource)]
24/// struct OrderBook {
25///     bids: Vec<(f64, f64)>,
26///     asks: Vec<(f64, f64)>,
27/// }
28/// ```
29#[proc_macro_derive(Resource)]
30pub fn derive_resource(input: TokenStream) -> TokenStream {
31    let input = parse_macro_input!(input as DeriveInput);
32    let name = &input.ident;
33    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
34
35    // Add Send + 'static where clause so errors point at the derive,
36    // not at the register() call site.
37    let mut bounds = where_clause.cloned();
38    let predicate: syn::WherePredicate = syn::parse_quote!(#name #ty_generics: Send + 'static);
39    bounds
40        .get_or_insert_with(|| syn::parse_quote!(where))
41        .predicates
42        .push(predicate);
43
44    quote! {
45        impl #impl_generics ::nexus_rt::Resource for #name #ty_generics
46            #bounds
47        {}
48    }
49    .into()
50}
51
52// =============================================================================
53// #[derive(Deref)]
54// =============================================================================
55
56/// Derive `Deref` for newtype wrappers.
57///
58/// - Single-field structs: auto-selects the field.
59/// - Multi-field structs: requires `#[deref]` on exactly one field.
60///
61/// ```ignore
62/// use nexus_rt::Deref;
63///
64/// #[derive(Deref)]
65/// struct MyWrapper(u64);
66///
67/// #[derive(Deref)]
68/// struct Named {
69///     #[deref]
70///     data: Vec<u8>,
71///     label: String,
72/// }
73/// ```
74#[proc_macro_derive(Deref, attributes(deref))]
75pub fn derive_deref(input: TokenStream) -> TokenStream {
76    let input = parse_macro_input!(input as DeriveInput);
77    let name = &input.ident;
78    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
79
80    let (field_ty, field_access) = match deref_field(&input.data, name) {
81        Ok(v) => v,
82        Err(e) => return e.to_compile_error().into(),
83    };
84
85    quote! {
86        impl #impl_generics ::core::ops::Deref for #name #ty_generics
87            #where_clause
88        {
89            type Target = #field_ty;
90
91            #[inline]
92            fn deref(&self) -> &Self::Target {
93                &self.#field_access
94            }
95        }
96    }
97    .into()
98}
99
100// =============================================================================
101// #[derive(DerefMut)]
102// =============================================================================
103
104/// Derive `DerefMut` for newtype wrappers.
105///
106/// Same field selection rules as `#[derive(Deref)]`. Must be used
107/// alongside `#[derive(Deref)]`.
108#[proc_macro_derive(DerefMut, attributes(deref))]
109pub fn derive_deref_mut(input: TokenStream) -> TokenStream {
110    let input = parse_macro_input!(input as DeriveInput);
111    let name = &input.ident;
112    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
113
114    let (_field_ty, field_access) = match deref_field(&input.data, name) {
115        Ok(v) => v,
116        Err(e) => return e.to_compile_error().into(),
117    };
118
119    quote! {
120        impl #impl_generics ::core::ops::DerefMut for #name #ty_generics
121            #where_clause
122        {
123            #[inline]
124            fn deref_mut(&mut self) -> &mut Self::Target {
125                &mut self.#field_access
126            }
127        }
128    }
129    .into()
130}
131
132// =============================================================================
133// Shared field resolution
134// =============================================================================
135
136/// Find the deref target field. Returns (field_type, field_access).
137fn deref_field(
138    data: &Data,
139    name: &syn::Ident,
140) -> Result<(syn::Type, proc_macro2::TokenStream), syn::Error> {
141    let fields = match data {
142        Data::Struct(s) => &s.fields,
143        Data::Enum(_) => {
144            return Err(syn::Error::new_spanned(
145                name,
146                "Deref/DerefMut can only be derived for structs, not enums",
147            ));
148        }
149        Data::Union(_) => {
150            return Err(syn::Error::new_spanned(
151                name,
152                "Deref/DerefMut can only be derived for structs, not unions",
153            ));
154        }
155    };
156
157    match fields {
158        // Tuple struct: single field → auto-select
159        Fields::Unnamed(f) if f.unnamed.len() == 1 => {
160            let field = f.unnamed.first().unwrap();
161            let ty = field.ty.clone();
162            let access = quote!(0);
163            Ok((ty, access))
164        }
165        // Named struct: single field → auto-select
166        Fields::Named(f) if f.named.len() == 1 => {
167            let field = f.named.first().unwrap();
168            let ty = field.ty.clone();
169            let ident = field.ident.as_ref().unwrap();
170            let access = quote!(#ident);
171            Ok((ty, access))
172        }
173        // Multiple fields → look for #[deref] attribute
174        Fields::Named(f) => {
175            let marked: Vec<_> = f
176                .named
177                .iter()
178                .filter(|field| field.attrs.iter().any(|a| a.path().is_ident("deref")))
179                .collect();
180
181            match marked.len() {
182                0 => Err(syn::Error::new_spanned(
183                    name,
184                    "multiple fields require exactly one `#[deref]` attribute",
185                )),
186                1 => {
187                    let field = marked[0];
188                    let ty = field.ty.clone();
189                    let ident = field.ident.as_ref().unwrap();
190                    let access = quote!(#ident);
191                    Ok((ty, access))
192                }
193                _ => Err(syn::Error::new_spanned(
194                    name,
195                    "only one field may have `#[deref]`",
196                )),
197            }
198        }
199        Fields::Unnamed(f) => {
200            let marked: Vec<_> = f
201                .unnamed
202                .iter()
203                .enumerate()
204                .filter(|(_, field)| field.attrs.iter().any(|a| a.path().is_ident("deref")))
205                .collect();
206
207            match marked.len() {
208                0 => Err(syn::Error::new_spanned(
209                    name,
210                    "multiple fields require exactly one `#[deref]` attribute",
211                )),
212                1 => {
213                    let (idx, field) = marked[0];
214                    let ty = field.ty.clone();
215                    let idx = syn::Index::from(idx);
216                    let access = quote!(#idx);
217                    Ok((ty, access))
218                }
219                _ => Err(syn::Error::new_spanned(
220                    name,
221                    "only one field may have `#[deref]`",
222                )),
223            }
224        }
225        Fields::Unit => Err(syn::Error::new_spanned(
226            name,
227            "Deref/DerefMut cannot be derived for unit structs",
228        )),
229    }
230}
231
232// =============================================================================
233// #[derive(Param)]
234// =============================================================================
235
236/// Derive the `Param` trait for a struct, enabling it to be used as a
237/// grouped handler parameter.
238///
239/// The struct must have exactly one lifetime parameter. Each field must
240/// implement `Param`, or be annotated with `#[param(ignore)]` (in which
241/// case it must implement `Default`).
242///
243/// ```ignore
244/// use nexus_rt::{Param, Res, ResMut, Local};
245///
246/// #[derive(Param)]
247/// struct TradingParams<'w> {
248///     book: Res<'w, OrderBook>,
249///     risk: ResMut<'w, RiskState>,
250///     local_count: Local<'w, u64>,
251/// }
252///
253/// fn on_order(params: TradingParams<'_>, order: Order) {
254///     // params.book, params.risk, params.local_count all available
255/// }
256/// ```
257#[proc_macro_derive(Param, attributes(param))]
258pub fn derive_param(input: TokenStream) -> TokenStream {
259    let input = parse_macro_input!(input as DeriveInput);
260    match derive_param_impl(&input) {
261        Ok(tokens) => tokens.into(),
262        Err(e) => e.to_compile_error().into(),
263    }
264}
265
266fn derive_param_impl(input: &DeriveInput) -> Result<proc_macro2::TokenStream, syn::Error> {
267    let name = &input.ident;
268
269    // Validate: must be a struct
270    let fields = match &input.data {
271        Data::Struct(s) => &s.fields,
272        _ => {
273            return Err(syn::Error::new_spanned(
274                name,
275                "derive(Param) can only be applied to structs",
276            ));
277        }
278    };
279
280    // Validate: exactly one lifetime parameter, no type/const generics
281    let lifetimes: Vec<_> = input.generics.lifetimes().collect();
282    if lifetimes.len() != 1 {
283        return Err(syn::Error::new_spanned(
284            &input.generics,
285            "derive(Param) requires exactly one lifetime parameter, \
286             e.g., `struct MyParam<'w>`",
287        ));
288    }
289    // TODO: support type and const generics by threading them through
290    // the generated State struct and Param impl (e.g., `Buffer<const N: usize>`).
291    // This is straightforward with syn's split_for_impl() but deferred to
292    // avoid the lifetime inference issues Bevy hit with generic SystemParams.
293    if input.generics.type_params().next().is_some()
294        || input.generics.const_params().next().is_some()
295    {
296        return Err(syn::Error::new_spanned(
297            &input.generics,
298            "derive(Param) does not yet support type or const generics — \
299             only a single lifetime parameter (e.g., `struct MyParam<'w>`). \
300             Use a concrete type instead (e.g., `Res<'w, Buffer<64>>` not `Res<'w, Buffer<N>>`)",
301        ));
302    }
303    let world_lifetime = &lifetimes[0].lifetime;
304
305    // Must be named fields
306    let named_fields = match fields {
307        Fields::Named(f) => &f.named,
308        _ => {
309            return Err(syn::Error::new_spanned(
310                name,
311                "derive(Param) requires named fields",
312            ));
313        }
314    };
315
316    // Classify fields: param fields (participate in init/fetch) vs ignored
317    let mut param_fields = Vec::new();
318    let mut ignored_fields = Vec::new();
319
320    for field in named_fields {
321        let field_name = field.ident.as_ref().unwrap();
322        let is_ignored = field.attrs.iter().any(|a| {
323            a.path().is_ident("param")
324                && a.meta
325                    .require_list()
326                    .is_ok_and(|l| l.tokens.to_string().trim() == "ignore")
327        });
328
329        if is_ignored {
330            ignored_fields.push(field_name);
331        } else {
332            // Substitute the struct's lifetime with 'static in the field type
333            let mut static_ty = field.ty.clone();
334            let mut replacer = LifetimeReplacer {
335                from: world_lifetime.ident.to_string(),
336            };
337            replacer.visit_type_mut(&mut static_ty);
338
339            param_fields.push((field_name, &field.ty, static_ty));
340        }
341    }
342
343    // Generate the State struct name
344    let state_name = format_ident!("{}State", name);
345
346    // State struct fields
347    let state_fields = param_fields.iter().map(|(field_name, _, static_ty)| {
348        quote! {
349            #field_name: <#static_ty as ::nexus_rt::Param>::State
350        }
351    });
352    let ignored_state_fields = ignored_fields.iter().map(|field_name| {
353        quote! {
354            #field_name: ()
355        }
356    });
357
358    // init() body
359    let init_fields = param_fields.iter().map(|(field_name, _, static_ty)| {
360        quote! {
361            #field_name: <#static_ty as ::nexus_rt::Param>::init(registry)
362        }
363    });
364    let init_ignored = ignored_fields.iter().map(|field_name| {
365        quote! { #field_name: () }
366    });
367
368    // fetch() body
369    let fetch_fields = param_fields.iter().map(|(field_name, _, static_ty)| {
370        quote! {
371            #field_name: <#static_ty as ::nexus_rt::Param>::fetch(world, &mut state.#field_name)
372        }
373    });
374    let fetch_ignored = ignored_fields.iter().map(|field_name| {
375        quote! {
376            #field_name: ::core::default::Default::default()
377        }
378    });
379
380    Ok(quote! {
381        #[doc(hidden)]
382        #[allow(non_camel_case_types)]
383        pub struct #state_name {
384            #(#state_fields,)*
385            #(#ignored_state_fields,)*
386        }
387
388        impl ::nexus_rt::Param for #name<'_> {
389            type State = #state_name;
390            type Item<'w> = #name<'w>;
391
392            fn init(registry: &::nexus_rt::Registry) -> Self::State {
393                #state_name {
394                    #(#init_fields,)*
395                    #(#init_ignored,)*
396                }
397            }
398
399            unsafe fn fetch<'w>(
400                world: &'w ::nexus_rt::World,
401                state: &'w mut Self::State,
402            ) -> #name<'w> {
403                #name {
404                    #(#fetch_fields,)*
405                    #(#fetch_ignored,)*
406                }
407            }
408        }
409    })
410}
411
412/// Replaces occurrences of a specific lifetime with `'static`.
413struct LifetimeReplacer {
414    from: String,
415}
416
417impl VisitMut for LifetimeReplacer {
418    fn visit_lifetime_mut(&mut self, lt: &mut Lifetime) {
419        if lt.ident == self.from {
420            *lt = Lifetime::new("'static", lt.apostrophe);
421        }
422    }
423}
424
425// =============================================================================
426// #[derive(View)]
427// =============================================================================
428
429/// Derive a `View` projection for use with pipeline `.view()` scopes.
430///
431/// Generates a marker ZST (`As{ViewName}`) and `unsafe impl View<Source>`
432/// for each `#[source(Type)]` attribute. Use with `.view::<AsViewName>()`
433/// in pipeline and DAG builders.
434///
435/// # Attributes
436///
437/// **On the struct:**
438/// - `#[source(TypePath)]` — one per source event type
439///
440/// **On fields:**
441/// - `#[borrow]` — borrow from source (`&source.field`) instead of copy
442/// - `#[source(TypePath, from = "name")]` — remap field name for a specific source
443///
444/// # Examples
445///
446/// ```ignore
447/// use nexus_rt::View;
448///
449/// #[derive(View)]
450/// #[source(NewOrderCommand)]
451/// #[source(AmendOrderCommand)]
452/// struct OrderView<'a> {
453///     #[borrow]
454///     symbol: &'a str,
455///     qty: u64,
456///     price: f64,
457/// }
458///
459/// // Generates: struct AsOrderView;
460/// // Generates: unsafe impl View<NewOrderCommand> for AsOrderView { ... }
461/// // Generates: unsafe impl View<AmendOrderCommand> for AsOrderView { ... }
462/// ```
463#[proc_macro_derive(View, attributes(source, borrow))]
464pub fn derive_view(input: TokenStream) -> TokenStream {
465    let input = parse_macro_input!(input as DeriveInput);
466    match derive_view_impl(&input) {
467        Ok(tokens) => tokens.into(),
468        Err(e) => e.to_compile_error().into(),
469    }
470}
471
472fn derive_view_impl(input: &DeriveInput) -> Result<proc_macro2::TokenStream, syn::Error> {
473    // Only structs
474    let fields = match &input.data {
475        Data::Struct(s) => match &s.fields {
476            Fields::Named(f) => &f.named,
477            _ => {
478                return Err(syn::Error::new_spanned(
479                    &input.ident,
480                    "#[derive(View)] only supports structs with named fields",
481                ));
482            }
483        },
484        _ => {
485            return Err(syn::Error::new_spanned(
486                &input.ident,
487                "#[derive(View)] can only be used on structs",
488            ));
489        }
490    };
491
492    let view_name = &input.ident;
493    let vis = &input.vis;
494
495    // Extract #[source(TypePath)] attributes from the struct
496    let sources = parse_source_attrs(&input.attrs, view_name)?;
497    if sources.is_empty() {
498        return Err(syn::Error::new_spanned(
499            view_name,
500            "#[derive(View)] requires at least one #[source(Type)] attribute",
501        ));
502    }
503
504    // Reject type and const generics
505    if input.generics.type_params().count() > 0 {
506        return Err(syn::Error::new_spanned(
507            &input.generics,
508            "#[derive(View)] does not support type parameters",
509        ));
510    }
511    if input.generics.const_params().count() > 0 {
512        return Err(syn::Error::new_spanned(
513            &input.generics,
514            "#[derive(View)] does not support const parameters",
515        ));
516    }
517
518    // Detect lifetime: 0 or 1 lifetime param
519    let lifetime_param = match input.generics.lifetimes().count() {
520        0 => None,
521        1 => Some(input.generics.lifetimes().next().unwrap().lifetime.clone()),
522        _ => {
523            return Err(syn::Error::new_spanned(
524                &input.generics,
525                "#[derive(View)] supports at most one lifetime parameter",
526            ));
527        }
528    };
529
530    // Marker name: As{ViewName}
531    let marker_name = format_ident!("As{}", view_name);
532
533    // Build ViewType<'a>, StaticViewType, and tick-lifetime tokens
534    let (view_type_with_a, static_view_type, view_type_tick) = lifetime_param.as_ref().map_or_else(
535        || {
536            (
537                quote! { #view_name },
538                quote! { #view_name },
539                quote! { #view_name },
540            )
541        },
542        |lt| {
543            let lt_ident = &lt.ident;
544            let mut static_generics = input.generics.clone();
545            LifetimeReplacer {
546                from: lt_ident.to_string(),
547            }
548            .visit_generics_mut(&mut static_generics);
549            let (_, static_ty_generics, _) = static_generics.split_for_impl();
550            (
551                quote! { #view_name<'a> },
552                quote! { #view_name #static_ty_generics },
553                quote! { #view_name<'_> },
554            )
555        },
556    );
557
558    // Parse field info
559    let field_infos: Vec<FieldInfo> = fields
560        .iter()
561        .map(parse_field_info)
562        .collect::<Result<_, _>>()?;
563
564    // Generate impl for each source
565    let mut impls = Vec::new();
566    for source_type in &sources {
567        let field_exprs: Vec<proc_macro2::TokenStream> = field_infos
568            .iter()
569            .map(|fi| {
570                let view_field = &fi.ident;
571                // Check for per-source field remap
572                let source_field = fi
573                    .remaps
574                    .iter()
575                    .find(|(path, _)| path_matches(path, source_type))
576                    .map_or_else(|| fi.ident.clone(), |(_, name)| format_ident!("{}", name));
577
578                if fi.borrow {
579                    quote! { #view_field: &source.#source_field }
580                } else {
581                    quote! { #view_field: source.#source_field }
582                }
583            })
584            .collect();
585
586        impls.push(quote! {
587            // SAFETY: ViewType<'a> and StaticViewType are the same struct
588            // with different lifetime parameters. Layout-identical by construction.
589            unsafe impl ::nexus_rt::View<#source_type> for #marker_name {
590                type ViewType<'a> = #view_type_with_a where #source_type: 'a;
591                type StaticViewType = #static_view_type;
592
593                fn view(source: &#source_type) -> #view_type_tick {
594                    #view_name {
595                        #(#field_exprs),*
596                    }
597                }
598            }
599        });
600    }
601
602    Ok(quote! {
603        /// View marker generated by `#[derive(View)]`.
604        #vis struct #marker_name;
605
606        #(#impls)*
607    })
608}
609
610struct FieldInfo {
611    ident: syn::Ident,
612    borrow: bool,
613    /// Per-source field remaps: (source_path, source_field_name)
614    remaps: Vec<(syn::Path, String)>,
615}
616
617fn parse_field_info(field: &syn::Field) -> Result<FieldInfo, syn::Error> {
618    let ident = field
619        .ident
620        .clone()
621        .ok_or_else(|| syn::Error::new_spanned(field, "View fields must be named"))?;
622
623    let borrow = field.attrs.iter().any(|a| a.path().is_ident("borrow"));
624
625    let mut remaps = Vec::new();
626    for attr in &field.attrs {
627        if attr.path().is_ident("source") {
628            // Parse #[source(TypePath, from = "field_name")]
629            attr.parse_args_with(|input: syn::parse::ParseStream| {
630                let path: syn::Path = input.parse()?;
631
632                if input.is_empty() {
633                    return Ok(());
634                }
635
636                input.parse::<syn::Token![,]>()?;
637                let kw: syn::Ident = input.parse()?;
638                if kw != "from" {
639                    return Err(syn::Error::new_spanned(&kw, "expected `from`"));
640                }
641                input.parse::<syn::Token![=]>()?;
642                let lit: syn::LitStr = input.parse()?;
643                remaps.push((path, lit.value()));
644                Ok(())
645            })?;
646        }
647    }
648
649    Ok(FieldInfo {
650        ident,
651        borrow,
652        remaps,
653    })
654}
655
656/// Parse `#[source(TypePath)]` attributes from struct-level attrs.
657fn parse_source_attrs(
658    attrs: &[syn::Attribute],
659    span_target: &syn::Ident,
660) -> Result<Vec<syn::Path>, syn::Error> {
661    let mut sources = Vec::new();
662    for attr in attrs {
663        if attr.path().is_ident("source") {
664            let path: syn::Path = attr.parse_args()?;
665            sources.push(path);
666        }
667    }
668    let _ = span_target; // used for error span if needed
669    Ok(sources)
670}
671
672/// Check if two paths match by comparing full path equality.
673fn path_matches(a: &syn::Path, b: &syn::Path) -> bool {
674    a == b
675}
676
677// =============================================================================
678// select! — compile-time dispatch table
679// =============================================================================
680
681/// Compile-time dispatch table for pipeline/DAG steps — the nexus-rt
682/// analogue of tokio's `select!`.
683///
684/// Eliminates the `resolve_step` + match-closure boilerplate by expanding
685/// to a literal `match` with pre-resolved monomorphized arms. Preserves
686/// exhaustiveness checking, jump table optimization, and zero-cost
687/// monomorphization.
688///
689/// # Grammar
690///
691/// ```text
692/// select! {
693///     <reg>,
694///     [ctx: <Type>,]          // callback mode (optional)
695///     [key: <closure>,]       // key extraction (optional)
696///     [project: <closure>,]   // input projection (optional, requires key:)
697///     <pattern> => <handler>,
698///     ...
699///     [_ => <default>,]       // fallthrough (optional, must be last)
700/// }
701/// ```
702///
703/// Or-patterns, literal patterns, and any other pattern rustc accepts
704/// work because the expansion is a real `match`.
705///
706/// # Three tiers of ceremony
707///
708/// **Tier 1** — input is the match value, arms take the input. No
709/// `key:`, no `project:`. Use when upstream has already classified
710/// the event down to a discriminant.
711///
712/// ```ignore
713/// select! {
714///     reg,
715///     OrderKind::New    => handle_new,
716///     OrderKind::Cancel => handle_cancel,
717/// }
718/// ```
719///
720/// **Tier 2** — input is a struct, match on a field, arms take the
721/// whole struct. The most common shape.
722///
723/// ```ignore
724/// select! {
725///     reg,
726///     key: |o: &Order| o.kind,
727///     OrderKind::New    => handle_new,
728///     OrderKind::Cancel => handle_cancel,
729/// }
730/// ```
731///
732/// **Tier 3** — input is a composite (e.g., a tuple), arms take a
733/// projection. Use when upstream emits both a discriminant and a
734/// payload side-by-side.
735///
736/// ```ignore
737/// select! {
738///     reg,
739///     key:     |(_, ct): &(Event, CmdType)| *ct,
740///     project: |(e, _)| e,
741///     CmdType::A => handle_a,
742///     CmdType::B => handle_b,
743///     _ => |_w, (e, ct)| log::error!("unsupported {:?} id={}", ct, e.id),
744/// }
745/// ```
746///
747/// # Callback form (with `ctx:`)
748///
749/// Adding `ctx: SomeContext` switches the expansion from
750/// `resolve_step` to `resolve_ctx_step` and threads `&mut SomeContext`
751/// through every arm. Works with `CtxPipelineBuilder` and
752/// `CtxDagBuilder`. All three tiers apply.
753///
754/// ```ignore
755/// select! {
756///     reg,
757///     ctx: SessionCtx,
758///     key: |o: &Order| o.kind,
759///     OrderKind::New    => on_new,    // fn(&mut SessionCtx, Order)
760///     OrderKind::Cancel => on_cancel,
761/// }
762/// ```
763///
764/// # `key:` closures need a type annotation
765///
766/// When `key:` is present, the closure parameter must have an explicit
767/// type annotation (e.g., `|o: &Order| o.kind`). Without it, rustc
768/// can't infer the input type at the point of key extraction. This is
769/// a fundamental Rust closure-inference limitation, not a macro issue.
770///
771/// `project:` closures do **not** need annotation — they're called
772/// inside match arms after `key:` has already constrained the input
773/// type.
774///
775/// # Performance
776///
777/// Zero overhead. The expansion is identical to the hand-written
778/// `let mut arm_N = resolve_step(...)` + closure + match pattern.
779/// `cargo asm` on `examples/select_asm_check.rs` confirms the
780/// dispatch compiles to a jump table for dense enum discriminants.
781///
782/// See `nexus-rt/docs/pipelines.md` and `nexus-rt/docs/callbacks.md`
783/// for full usage guides.
784#[proc_macro]
785pub fn select(input: TokenStream) -> TokenStream {
786    let parsed = parse_macro_input!(input as select::SelectInput);
787    select::expand(&parsed).into()
788}