Skip to main content

mirui_macros/
lib.rs

1extern crate proc_macro;
2
3mod compose;
4
5use proc_macro::TokenStream;
6use quote::quote;
7use syn::parse_macro_input;
8
9use xrune::ds_node::ds_attr::DsAttr;
10use xrune::ds_node::{DsRoot, DsTreeRef};
11use xrune::ds_rune::DsRune;
12use xrune::ds_rune::decipher::decipher;
13
14enum Cmd {
15    Widget(WidgetCmd),
16    Iter(IterCmd),
17    If(IfCmd),
18}
19
20struct WidgetCmd {
21    var: syn::Ident,
22    attrs: Vec<proc_macro2::TokenStream>,
23    layout_fields: Vec<proc_macro2::TokenStream>,
24    errors: Vec<proc_macro2::TokenStream>,
25    enchants: Vec<proc_macro2::TokenStream>,
26    children: Vec<Cmd>,
27}
28
29struct IterCmd {
30    iterable: proc_macro2::TokenStream,
31    variable: syn::Ident,
32    body: Vec<Cmd>,
33}
34
35struct IfCmd {
36    condition: proc_macro2::TokenStream,
37    body: Vec<Cmd>,
38}
39
40struct MiruiRune {
41    world_expr: proc_macro2::TokenStream,
42    parent_expr: proc_macro2::TokenStream,
43    stack: Vec<Vec<Cmd>>,
44    counter: usize,
45}
46
47impl MiruiRune {
48    fn new() -> Self {
49        Self {
50            world_expr: quote! { __world },
51            parent_expr: quote! { __parent },
52            stack: vec![Vec::new()],
53            counter: 0,
54        }
55    }
56
57    fn next_var(&mut self) -> syn::Ident {
58        let name = format!("__w{}", self.counter);
59        self.counter += 1;
60        syn::Ident::new(&name, proc_macro2::Span::call_site())
61    }
62
63    fn parse_attrs(
64        attrs: &[DsAttr],
65    ) -> (
66        Vec<proc_macro2::TokenStream>,
67        Vec<proc_macro2::TokenStream>,
68        Vec<proc_macro2::TokenStream>,
69        Vec<proc_macro2::TokenStream>,
70    ) {
71        let mut builder_calls = Vec::new();
72        let mut layout_fields = Vec::new();
73        let mut errors = Vec::new();
74        let component_inserts = Vec::new();
75
76        for attr in attrs {
77            let name = attr.name.to_string();
78            let value = &attr.value;
79            match name.as_str() {
80                "bg_color" => builder_calls.push(quote! { .bg_color(#value) }),
81                "text" => builder_calls.push(quote! { .text(#value) }),
82                "text_color" => builder_calls.push(quote! { .text_color(#value) }),
83                "border_radius" => builder_calls.push(quote! { .border_radius(#value) }),
84                "clip_children" => builder_calls.push(quote! { .clip_children(#value) }),
85                "border_color" => builder_calls.push(quote! { .border(#value, 1) }),
86                "border_width" => builder_calls.push(quote! { .border_width(#value) }),
87                "width" => layout_fields.push(quote! { width: mirui::types::Dimension::Px(mirui::types::Fixed::from_int(#value as i32)) }),
88                "height" => layout_fields.push(quote! { height: mirui::types::Dimension::Px(mirui::types::Fixed::from_int(#value as i32)) }),
89                "grow" => layout_fields.push(quote! { grow: mirui::types::Fixed::from_f32(#value) }),
90                "direction" => layout_fields.push(quote! { direction: #value }),
91                "justify" => layout_fields.push(quote! { justify: #value }),
92                "align" => layout_fields.push(quote! { align: #value }),
93                "padding" => layout_fields.push(quote! { padding: #value }),
94                "position" => layout_fields.push(quote! { position: #value }),
95                "left" => layout_fields.push(quote! { left: mirui::types::Dimension::Px(mirui::types::Fixed::from_int(#value)) }),
96                "top" => layout_fields.push(quote! { top: mirui::types::Dimension::Px(mirui::types::Fixed::from_int(#value)) }),
97                "image" => builder_calls.push(quote! { .image(#value) }),
98                unknown => {
99                    let msg = format!("unknown widget attribute `{unknown}`");
100                    errors.push(syn::Error::new(attr.name.span(), msg).to_compile_error());
101                }
102            }
103        }
104        (builder_calls, layout_fields, errors, component_inserts)
105    }
106
107    /// Emit a Cmd, returning generated tokens.
108    /// `parent_var` is the variable name of the parent widget that children attach to.
109    fn emit_cmd(
110        cmd: &Cmd,
111        world: &proc_macro2::TokenStream,
112        parent_var: &proc_macro2::TokenStream,
113    ) -> proc_macro2::TokenStream {
114        match cmd {
115            Cmd::Widget(w) => Self::emit_widget(w, world),
116            Cmd::Iter(i) => Self::emit_iter(i, world, parent_var),
117            Cmd::If(i) => Self::emit_if(i, world, parent_var),
118        }
119    }
120
121    fn emit_widget(cmd: &WidgetCmd, world: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
122        let var = &cmd.var;
123        let attrs = &cmd.attrs;
124        let layout_fields = &cmd.layout_fields;
125        let errors = &cmd.errors;
126
127        let mut tokens = proc_macro2::TokenStream::new();
128
129        for e in errors {
130            tokens.extend(e.clone());
131        }
132
133        // Emit static widget children first (post-order)
134        let var_ts = quote! { #var };
135        let mut child_vars = Vec::new();
136        let mut deferred_iters = Vec::new();
137
138        for child in &cmd.children {
139            match child {
140                Cmd::Widget(w) => {
141                    tokens.extend(Self::emit_widget(w, world));
142                    child_vars.push(&w.var);
143                }
144                Cmd::Iter(_) | Cmd::If(_) => {
145                    deferred_iters.push(child);
146                }
147            }
148        }
149
150        // Create this widget with static children
151        let layout_call = if layout_fields.is_empty() {
152            quote! {}
153        } else {
154            quote! { .layout(mirui::layout::LayoutStyle { #(#layout_fields,)* ..Default::default() }) }
155        };
156
157        let child_calls: Vec<proc_macro2::TokenStream> =
158            child_vars.iter().map(|c| quote! { .child(#c) }).collect();
159
160        tokens.extend(quote! {
161            let #var = mirui::widget::builder::WidgetBuilder::new(#world)
162                #(#attrs)*
163                #layout_call
164                #(#child_calls)*
165                .id();
166        });
167
168        // Emit enchants — insert components
169        let enchants = &cmd.enchants;
170        for enchant in enchants {
171            tokens.extend(quote! {
172                (#world).insert(#var, #enchant);
173            });
174        }
175
176        // Now emit iter children — they attach dynamically to this widget
177        for iter_cmd in deferred_iters {
178            tokens.extend(Self::emit_cmd(iter_cmd, world, &var_ts));
179        }
180
181        tokens
182    }
183
184    fn emit_iter(
185        cmd: &IterCmd,
186        world: &proc_macro2::TokenStream,
187        parent_var: &proc_macro2::TokenStream,
188    ) -> proc_macro2::TokenStream {
189        let iterable = &cmd.iterable;
190        let variable = &cmd.variable;
191
192        // Generate loop body — each widget in body attaches to parent_var
193        let mut body_tokens = proc_macro2::TokenStream::new();
194        for child in &cmd.body {
195            body_tokens.extend(Self::emit_cmd(child, world, parent_var));
196            // Attach each top-level widget in loop body to parent
197            if let Cmd::Widget(w) = child {
198                let child_var = &w.var;
199                body_tokens.extend(quote! {
200                    {
201                        use mirui::widget::{Children, Parent};
202                        (#world).insert(#child_var, Parent(#parent_var));
203                        if let Some(children) = (#world).get_mut::<Children>(#parent_var) {
204                            children.0.push(#child_var);
205                        }
206                    }
207                });
208            }
209        }
210
211        quote! {
212            for #variable in #iterable {
213                #body_tokens
214            }
215        }
216    }
217
218    fn emit_if(
219        cmd: &IfCmd,
220        world: &proc_macro2::TokenStream,
221        parent_var: &proc_macro2::TokenStream,
222    ) -> proc_macro2::TokenStream {
223        let condition = &cmd.condition;
224
225        let mut body_tokens = proc_macro2::TokenStream::new();
226        for child in &cmd.body {
227            body_tokens.extend(Self::emit_cmd(child, world, parent_var));
228            if let Cmd::Widget(w) = child {
229                let child_var = &w.var;
230                body_tokens.extend(quote! {
231                    {
232                        use mirui::widget::{Children, Parent};
233                        (#world).insert(#child_var, Parent(#parent_var));
234                        if let Some(children) = (#world).get_mut::<Children>(#parent_var) {
235                            children.0.push(#child_var);
236                        }
237                    }
238                });
239            }
240        }
241
242        quote! {
243            if #condition {
244                #body_tokens
245            }
246        }
247    }
248}
249
250impl DsRune for MiruiRune {
251    fn inscribe_root(&mut self, _parent_expr: &syn::Expr) {}
252
253    fn inscribe_widget(
254        &mut self,
255        _name: &syn::Ident,
256        attrs: &[DsAttr],
257        enchants: &[syn::Expr],
258        children: &[DsTreeRef],
259    ) {
260        let var = self.next_var();
261        let (builder_calls, layout_fields, errors, component_inserts) = Self::parse_attrs(attrs);
262        let mut enchant_tokens: Vec<proc_macro2::TokenStream> = component_inserts;
263        enchant_tokens.extend(enchants.iter().map(|e| quote! { #e }));
264
265        self.stack.push(Vec::new());
266        for child in children {
267            decipher(child, self);
268        }
269        let my_children = self.stack.pop().unwrap();
270
271        let cmd = Cmd::Widget(WidgetCmd {
272            var,
273            attrs: builder_calls,
274            layout_fields,
275            errors,
276            enchants: enchant_tokens,
277            children: my_children,
278        });
279
280        self.stack.last_mut().unwrap().push(cmd);
281    }
282
283    fn inscribe_if(&mut self, condition: &syn::Expr, children: &[DsTreeRef]) {
284        self.stack.push(Vec::new());
285        for child in children {
286            decipher(child, self);
287        }
288        let body = self.stack.pop().unwrap();
289
290        let cmd = Cmd::If(IfCmd {
291            condition: quote! { #condition },
292            body,
293        });
294
295        self.stack.last_mut().unwrap().push(cmd);
296    }
297
298    fn inscribe_iter(
299        &mut self,
300        iterable: &syn::Expr,
301        variable: &syn::Ident,
302        children: &[DsTreeRef],
303    ) {
304        self.stack.push(Vec::new());
305        for child in children {
306            decipher(child, self);
307        }
308        let body = self.stack.pop().unwrap();
309
310        let cmd = Cmd::Iter(IterCmd {
311            iterable: quote! { #iterable },
312            variable: variable.clone(),
313            body,
314        });
315
316        self.stack.last_mut().unwrap().push(cmd);
317    }
318
319    fn seal(self) -> proc_macro2::TokenStream {
320        let world = &self.world_expr;
321        let parent_entity = &self.parent_expr;
322        let mut tokens = proc_macro2::TokenStream::new();
323
324        let root_cmds = &self.stack[0];
325        for cmd in root_cmds {
326            tokens.extend(Self::emit_cmd(cmd, world, parent_entity));
327        }
328
329        // Attach top-level widgets to parent (iter attaches inside its own loop)
330        let mut last_var = None;
331        for cmd in root_cmds {
332            if let Cmd::Widget(w) = cmd {
333                let var = &w.var;
334                last_var = Some(var.clone());
335                tokens.extend(quote! {
336                    {
337                        use mirui::widget::{Children, Parent};
338                        (#world).insert(#var, Parent(#parent_entity));
339                        if let Some(children) = (#world).get_mut::<Children>(#parent_entity) {
340                            children.0.push(#var);
341                        }
342                    }
343                });
344            }
345        }
346
347        // Return the top-level widget entity
348        if let Some(var) = last_var {
349            quote! { { #tokens #var } }
350        } else {
351            quote! { { #tokens } }
352        }
353    }
354}
355
356#[proc_macro]
357pub fn ui(input: TokenStream) -> TokenStream {
358    let root = parse_macro_input!(input as DsRoot);
359    let mut rune = MiruiRune::new();
360
361    let context_attrs = root.get_context_attrs();
362    if let Some(world_attr) = context_attrs.iter().find(|a| a.name == "world") {
363        let world_expr = &world_attr.value;
364        rune.world_expr = quote! { #world_expr };
365    } else {
366        return syn::Error::new(proc_macro2::Span::call_site(), "missing `world` in context")
367            .to_compile_error()
368            .into();
369    }
370
371    let parent = root.get_parent();
372    rune.parent_expr = quote! { #parent };
373
374    rune.inscribe_root(&root.get_parent());
375    let content = root.get_content();
376    decipher(&content, &mut rune);
377    TokenStream::from(rune.seal())
378}
379
380#[proc_macro]
381pub fn compose_backend(input: TokenStream) -> TokenStream {
382    compose::expand(input.into()).into()
383}
384
385/// ```rust,ignore
386/// timer!(Cycle, every: 3_000, |world, entity| { /* ... */ });
387/// // schedule: after: ms | every: ms | repeat: N every: ms | until: D every: ms
388/// ```
389///
390/// Sugar over `Timer::after / every / repeat / until`; all four share
391/// the generic `timer_system`, so N invocations don't grow the binary.
392#[proc_macro]
393pub fn timer(input: TokenStream) -> TokenStream {
394    timer_impl::expand(input.into()).into()
395}
396
397mod timer_impl {
398    use proc_macro2::TokenStream;
399    use quote::quote;
400    use syn::parse::{Parse, ParseStream};
401    use syn::{ExprClosure, Ident, LitInt, Token, parse2};
402
403    enum Schedule {
404        After(LitInt),
405        Every(LitInt),
406        Repeat { times: LitInt, period: LitInt },
407        Until { deadline: LitInt, period: LitInt },
408    }
409
410    struct TimerInput {
411        name: Ident,
412        schedule: Schedule,
413        closure: ExprClosure,
414    }
415
416    impl Parse for Schedule {
417        fn parse(input: ParseStream) -> syn::Result<Self> {
418            let kind: Ident = input.parse()?;
419            input.parse::<Token![:]>()?;
420            let first: LitInt = input.parse()?;
421            match kind.to_string().as_str() {
422                "after" => Ok(Schedule::After(first)),
423                "every" => Ok(Schedule::Every(first)),
424                "repeat" => {
425                    // `repeat: N every: M`
426                    let kw: Ident = input.parse()?;
427                    if kw != "every" {
428                        return Err(syn::Error::new(
429                            kw.span(),
430                            "expected `every` after `repeat: N`",
431                        ));
432                    }
433                    input.parse::<Token![:]>()?;
434                    let period: LitInt = input.parse()?;
435                    Ok(Schedule::Repeat {
436                        times: first,
437                        period,
438                    })
439                }
440                "until" => {
441                    // `until: deadline every: period`
442                    let kw: Ident = input.parse()?;
443                    if kw != "every" {
444                        return Err(syn::Error::new(
445                            kw.span(),
446                            "expected `every` after `until: D`",
447                        ));
448                    }
449                    input.parse::<Token![:]>()?;
450                    let period: LitInt = input.parse()?;
451                    Ok(Schedule::Until {
452                        deadline: first,
453                        period,
454                    })
455                }
456                other => Err(syn::Error::new(
457                    kind.span(),
458                    format!(
459                        "unknown schedule keyword `{other}`; expected after / every / repeat / until"
460                    ),
461                )),
462            }
463        }
464    }
465
466    impl Parse for TimerInput {
467        fn parse(input: ParseStream) -> syn::Result<Self> {
468            let name: Ident = input.parse()?;
469            input.parse::<Token![,]>()?;
470            let schedule: Schedule = input.parse()?;
471            input.parse::<Token![,]>()?;
472            let closure: ExprClosure = input.parse()?;
473            Ok(Self {
474                name,
475                schedule,
476                closure,
477            })
478        }
479    }
480
481    pub fn expand(input: TokenStream) -> TokenStream {
482        let parsed = match parse2::<TimerInput>(input) {
483            Ok(v) => v,
484            Err(e) => return e.to_compile_error(),
485        };
486
487        let name = &parsed.name;
488        let closure = &parsed.closure;
489
490        let ctor = match &parsed.schedule {
491            Schedule::After(p) => quote! { mirui::timer::Timer::after(#p, __cb) },
492            Schedule::Every(p) => quote! { mirui::timer::Timer::every(#p, __cb) },
493            Schedule::Repeat { times, period } => {
494                quote! { mirui::timer::Timer::repeat(#times, #period, __cb) }
495            }
496            Schedule::Until { deadline, period } => {
497                quote! { mirui::timer::Timer::until(#deadline, #period, __cb) }
498            }
499        };
500
501        quote! {
502            pub struct #name;
503            impl #name {
504                pub fn install(world: &mut mirui::ecs::World) -> mirui::ecs::Entity {
505                    let __cb: fn(&mut mirui::ecs::World, mirui::ecs::Entity) = #closure;
506                    let e = world.spawn();
507                    world.insert(e, #ctor);
508                    e
509                }
510            }
511        }
512    }
513}
514
515/// Define a motion component (Tween or Spring) + its tick/apply system.
516///
517/// ```rust,ignore
518/// animate!(AnimateX, |world, entity, value| {
519///     mirui::widget::set_position(world, entity, value, Fixed::from_int(2));
520/// });
521///
522/// // Generated:
523/// // - struct AnimateX(pub mirui::anim::Motion)
524/// // - impl MotionComponent for AnimateX
525/// // - AnimateX::system() -> fn(&mut World)
526/// //
527/// // Usage:
528/// //   app.add_system(AnimateX::system());
529/// //   world.insert(e, AnimateX(Tween::ease_to(from, to, 250).into()));
530/// //   world.insert(e, AnimateX(Spring::preset(from, to, SMOOTH).into()));
531/// ```
532#[proc_macro]
533pub fn animate(input: TokenStream) -> TokenStream {
534    animate_impl::expand(input.into()).into()
535}
536
537mod animate_impl {
538    use proc_macro2::TokenStream;
539    use quote::quote;
540    use syn::parse::{Parse, ParseStream};
541    use syn::{ExprClosure, Ident, Token, parse2};
542
543    struct AnimateInput {
544        name: Ident,
545        closure: ExprClosure,
546    }
547
548    impl Parse for AnimateInput {
549        fn parse(input: ParseStream) -> syn::Result<Self> {
550            let name: Ident = input.parse()?;
551            input.parse::<Token![,]>()?;
552            let closure: ExprClosure = input.parse()?;
553            Ok(Self { name, closure })
554        }
555    }
556
557    pub fn expand(input: TokenStream) -> TokenStream {
558        let parsed = match parse2::<AnimateInput>(input) {
559            Ok(v) => v,
560            Err(e) => return e.to_compile_error(),
561        };
562
563        let name = &parsed.name;
564        let closure = &parsed.closure;
565
566        quote! {
567            pub struct #name(pub mirui::anim::Motion);
568
569            impl mirui::anim::MotionComponent for #name {
570                fn motion(&self) -> &mirui::anim::Motion { &self.0 }
571                fn motion_mut(&mut self) -> &mut mirui::anim::Motion { &mut self.0 }
572            }
573
574            impl #name {
575                pub fn system() -> fn(&mut mirui::ecs::World) {
576                    fn __sys(world: &mut mirui::ecs::World) {
577                        mirui::anim::run_motion::<#name>(world, #closure);
578                    }
579                    __sys
580                }
581            }
582        }
583    }
584}
585
586/// Mints unique guard idents for `trace_span!` so multiple calls in
587/// the same scope don't shadow each other. Overflow at 2³² is
588/// theoretical only.
589static TRACE_SPAN_COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
590
591fn next_trace_span_id() -> u32 {
592    TRACE_SPAN_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
593}
594
595mod trace_span_input {
596    use syn::parse::{Parse, ParseStream};
597    use syn::{Block, LitStr, Token};
598
599    pub enum TraceSpanInput {
600        Statement(LitStr),
601        Expression(LitStr, Block),
602    }
603
604    impl Parse for TraceSpanInput {
605        fn parse(input: ParseStream) -> syn::Result<Self> {
606            let name: LitStr = input.parse()?;
607            if input.is_empty() {
608                Ok(TraceSpanInput::Statement(name))
609            } else {
610                input.parse::<Token![,]>()?;
611                let body: Block = input.parse()?;
612                Ok(TraceSpanInput::Expression(name, body))
613            }
614        }
615    }
616}
617
618/// `trace_span!("name")` — RAII statement: guard lives until end of
619/// scope. Multiple calls in one scope each get a unique binding.
620///
621/// `trace_span!("name", { ... })` — block expression form,
622/// evaluates to the block's value.
623#[proc_macro]
624pub fn trace_span(input: TokenStream) -> TokenStream {
625    let parsed = parse_macro_input!(input as trace_span_input::TraceSpanInput);
626    match parsed {
627        trace_span_input::TraceSpanInput::Statement(name) => {
628            let id = next_trace_span_id();
629            let ident = quote::format_ident!("__trace_span_guard_{}", id);
630            quote::quote! {
631                let #ident = mirui::perf::enter(#name);
632            }
633            .into()
634        }
635        trace_span_input::TraceSpanInput::Expression(name, body) => {
636            let id = next_trace_span_id();
637            let ident = quote::format_ident!("__trace_span_guard_{}", id);
638            quote::quote! {{
639                let #ident = mirui::perf::enter(#name);
640                let __trace_span_value = #body;
641                drop(#ident);
642                __trace_span_value
643            }}
644            .into()
645        }
646    }
647}
648
649/// `#[trace_fn("name")]` — equivalent to `trace_span!("name");` as
650/// the first statement of the fn body.
651#[proc_macro_attribute]
652pub fn trace_fn(args: TokenStream, item: TokenStream) -> TokenStream {
653    let name = parse_macro_input!(args as syn::LitStr);
654    let mut func = parse_macro_input!(item as syn::ItemFn);
655    let stmts = &func.block.stmts;
656    let id = next_trace_span_id();
657    let ident = quote::format_ident!("__trace_span_guard_{}", id);
658    *func.block = syn::parse_quote! {{
659        let #ident = mirui::perf::enter(#name);
660        #(#stmts)*
661    }};
662    quote::quote! { #func }.into()
663}
664
665mod system_attr {
666    use syn::parse::{Parse, ParseStream};
667    use syn::{Expr, Ident, LitStr, Token, Type, bracketed, punctuated::Punctuated};
668
669    pub struct SystemArgs {
670        pub name: Option<LitStr>,
671        pub order: Option<Expr>,
672        /// Component type(s) gating this system. Empty = always runs.
673        /// Multiple entries are OR-combined (any present triggers run).
674        pub expect: Vec<Type>,
675    }
676
677    impl Parse for SystemArgs {
678        fn parse(input: ParseStream) -> syn::Result<Self> {
679            let mut name: Option<LitStr> = None;
680            let mut order: Option<Expr> = None;
681            let mut expect: Vec<Type> = Vec::new();
682            while !input.is_empty() {
683                let key: Ident = input.parse()?;
684                input.parse::<Token![=]>()?;
685                match key.to_string().as_str() {
686                    "name" => name = Some(input.parse()?),
687                    "order" => order = Some(input.parse()?),
688                    "expect" => {
689                        if input.peek(syn::token::Bracket) {
690                            let content;
691                            bracketed!(content in input);
692                            let types: Punctuated<Type, Token![,]> =
693                                content.parse_terminated(Type::parse, Token![,])?;
694                            expect.extend(types);
695                        } else {
696                            expect.push(input.parse()?);
697                        }
698                    }
699                    other => {
700                        return Err(syn::Error::new(
701                            key.span(),
702                            format!(
703                                "unknown #[system] arg `{other}`; expected `name`, `order`, or `expect`",
704                            ),
705                        ));
706                    }
707                }
708                if input.is_empty() {
709                    break;
710                }
711                input.parse::<Token![,]>()?;
712            }
713            Ok(Self {
714                name,
715                order,
716                expect,
717            })
718        }
719    }
720}
721
722/// Attach perf-aware metadata to a `fn(&mut World)`.
723///
724/// Generates a sibling module sharing the fn's ident exposing
725/// `system()` returning a [`mirui::ecs::System`] with the configured
726/// name + run_order slot. The fn itself is left intact so direct
727/// calls (tests, manual invocation) still work.
728///
729/// ```ignore
730/// #[mirui::system(order = ANIMATION)]
731/// fn spin_system(world: &mut World) { /* ... */ }
732///
733/// spin_system(world);                     // direct call
734/// app.add_system(spin_system::system());  // scheduled
735/// ```
736///
737/// Defaults: `name` derives from the fn ident; `order` defaults to
738/// `run_order::NORMAL`. `order` accepts either a `run_order::*`
739/// constant or a literal `i32`.
740#[proc_macro_attribute]
741pub fn system(args: TokenStream, item: TokenStream) -> TokenStream {
742    let args = parse_macro_input!(args as system_attr::SystemArgs);
743    let func = parse_macro_input!(item as syn::ItemFn);
744    let fn_ident = &func.sig.ident;
745    let fn_vis = &func.vis;
746    let name_lit = args
747        .name
748        .unwrap_or_else(|| syn::LitStr::new(&fn_ident.to_string(), fn_ident.span()));
749    let order_expr: syn::Expr = match args.order {
750        Some(e) => e,
751        None => syn::parse_quote!(mirui::ecs::run_order::NORMAL),
752    };
753    let expect_const_ident =
754        quote::format_ident!("__MIRUI_EXPECT_{}", fn_ident.to_string().to_uppercase());
755    let expect_outer = if args.expect.is_empty() {
756        quote::quote! {}
757    } else {
758        let entries = args.expect.iter().map(|ty| {
759            quote::quote! { (::core::any::TypeId::of::<#ty>) as fn() -> ::core::any::TypeId }
760        });
761        quote::quote! {
762            #[doc(hidden)]
763            #[allow(non_upper_case_globals)]
764            const #expect_const_ident: &[fn() -> ::core::any::TypeId] = &[ #(#entries),* ];
765        }
766    };
767    let with_expect_call = if args.expect.is_empty() {
768        quote::quote! {}
769    } else {
770        quote::quote! { .with_expect(super::#expect_const_ident) }
771    };
772    quote::quote! {
773        #func
774        #expect_outer
775
776        #[allow(non_snake_case, non_camel_case_types)]
777        #fn_vis mod #fn_ident {
778            #[allow(unused_imports)]
779            use mirui::ecs::run_order::*;
780            pub const fn system() -> mirui::ecs::System {
781                mirui::ecs::System::new(#name_lit, #order_expr, super::#fn_ident) #with_expect_call
782            }
783        }
784    }
785    .into()
786}