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 = <.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}