message_bus_macros/
codegen.rs

1use crate::{
2    analysis::Analysis,
3    ast::{Ast, SubTopic, Topic},
4};
5use proc_macro2::{Span, TokenStream as TokenStream2};
6use quote::quote;
7use syn::Ident;
8
9fn make_topics_enum(name: &Ident, topics: &[Topic], sub_topics: &[SubTopic]) -> TokenStream2 {
10    let mut arms = Vec::new();
11
12    for topic in topics {
13        let tn = &topic.name;
14        let tp = &topic.payload;
15        let doc = format!("Type-level definition of the `{tn}` topic");
16
17        arms.push(quote!(
18            #[doc = #doc]
19            #tn(#tp)
20        ));
21    }
22
23    for sub_topic in sub_topics {
24        let tn = &sub_topic.name;
25        let doc = format!("Type-level definition of the `{tn}` sub-topic");
26
27        arms.push(quote!(
28            #[doc = #doc]
29            #tn(#tn)
30        ));
31    }
32
33    let doc = format!("Type-level definition of all topics in `{name}`");
34    quote!(
35        #[doc = #doc]
36        #[derive(Clone, Debug)]
37        pub enum #name {
38            #(#arms),*
39        }
40    )
41}
42
43fn codegen_topics(topics: &[Topic], subtopic_tracker: &mut SubTopicTracker) -> Vec<TokenStream2> {
44    let mut tokens = Vec::new();
45
46    for topic in topics {
47        let topic_name = &topic.name;
48        let topic_payload = &topic.payload;
49        let topic_static = Ident::new(&format!("__TOPIC_{topic_name}"), Span::call_site());
50        let topic_capacity = &topic.capacity;
51
52        let doc_topic = format!("Handle to the `{topic_name}` topic.");
53        let doc_sub = format!("Subscribe to the `{topic_name}` topic.");
54        let doc_pub = format!("Publish to the `{topic_name}` topic.");
55
56        let publish_parent_topics = subtopic_tracker.to_parent_publishes(topic_name);
57
58        tokens.push(quote!(
59            #[doc = #doc_topic]
60            pub struct #topic_name;
61
62            #[doc(hidden)]
63            #[allow(non_upper_case_globals)]
64            static #topic_static: ::make_message_bus::Topic<#topic_payload> = ::make_message_bus::Topic::new::<#topic_capacity>();
65
66            impl #topic_name {
67                #[doc = #doc_sub]
68                pub fn subscribe() -> ::make_message_bus::Subscriber<#topic_payload> {
69                    #topic_static.subscribe()
70                }
71
72                #[doc = #doc_pub]
73                pub fn publish(payload: #topic_payload) {
74                    #(#publish_parent_topics)*
75
76                    #topic_static.publish(payload);
77                }
78            }
79        ));
80    }
81
82    tokens
83}
84
85fn codegen_subtopics(
86    sub_topics: &[SubTopic],
87    subtopic_tracker: &mut SubTopicTracker,
88) -> Vec<TokenStream2> {
89    let mut tokens = Vec::new();
90
91    for sub_topic in sub_topics {
92        subtopic_tracker.add_subtopic(sub_topic.name.clone());
93
94        let topic_enum = make_topics_enum(
95            &sub_topic.name,
96            &sub_topic.ast.topics,
97            &sub_topic.ast.sub_topics,
98        );
99        let topics = codegen_topics(&sub_topic.ast.topics, subtopic_tracker);
100
101        // For the next sub topic, recurse down the tree until bottom is reached
102        let sub_topic_tokens = codegen_subtopics(&sub_topic.ast.sub_topics, subtopic_tracker);
103        let sub_topic_name = &sub_topic.name;
104        let sub_topic_module = &sub_topic.module;
105        let sub_topic_doc1 = format!(
106            "Module containing topics and implementation for the `{sub_topic_name}` subtopic"
107        );
108        let sub_topic_doc2 =
109            format!("All topics in the `{sub_topic_module}::{sub_topic_name}` subtopic");
110        let sub_topic_static = Ident::new(&format!("__TOPIC_{sub_topic_name}"), Span::call_site());
111
112        let pub_use = if !subtopic_tracker.at_root() {
113            quote!(pub use #sub_topic_module::#sub_topic_name;)
114        } else {
115            quote!()
116        };
117
118        let doc_sub = format!("Subscribe to the `{sub_topic_name}` sub-topic.");
119
120        let mut capacity = 0;
121        find_total_capacity(sub_topic, &mut capacity);
122
123        tokens.push(quote!(
124            #pub_use
125
126            #[doc = #sub_topic_doc1]
127            pub mod #sub_topic_module {
128                #[doc(hidden)]
129                #[allow(non_upper_case_globals)]
130                static #sub_topic_static: ::make_message_bus::Topic<#sub_topic_name> = ::make_message_bus::Topic::new::<#capacity>();
131
132                #[doc = #sub_topic_doc2]
133                #topic_enum
134
135                impl #sub_topic_name {
136                    #[doc = #doc_sub]
137                    pub fn subscribe() -> ::make_message_bus::Subscriber<#sub_topic_name> {
138                        #sub_topic_static.subscribe()
139                    }
140                }
141
142                #(#topics)*
143
144                #(#sub_topic_tokens)*
145            }
146        ));
147        subtopic_tracker.remove_last_subtopic();
148    }
149
150    tokens
151}
152
153fn find_total_capacity(sub_topic: &SubTopic, capacity: &mut usize) {
154    let topic_cap: usize = sub_topic
155        .ast
156        .topics
157        .iter()
158        .map(|topic| topic.capacity)
159        .sum();
160
161    *capacity += topic_cap;
162
163    for st in &sub_topic.ast.sub_topics {
164        find_total_capacity(st, capacity);
165    }
166}
167
168struct SubTopicTracker(Vec<Ident>);
169
170impl SubTopicTracker {
171    pub fn new() -> Self {
172        Self(Vec::new())
173    }
174
175    pub fn depth(&self) -> usize {
176        self.0.len()
177    }
178
179    pub fn at_root(&self) -> bool {
180        self.depth() < 2
181    }
182
183    pub fn add_subtopic(&mut self, subtopic: Ident) {
184        self.0.push(subtopic);
185    }
186
187    pub fn remove_last_subtopic(&mut self) {
188        self.0.pop();
189    }
190
191    pub fn to_parent_publishes(&self, current_topic: &Ident) -> Vec<TokenStream2> {
192        let mut publish_tokens = Vec::new();
193
194        let mut super_tokens = quote!();
195        let mut payload = quote!(payload.clone());
196        let mut last_topic = current_topic;
197
198        for parent_topic in self.0.iter().rev() {
199            let parent_topic_static =
200                Ident::new(&format!("__TOPIC_{parent_topic}"), Span::call_site());
201
202            payload = quote!(#super_tokens #parent_topic::#last_topic(#payload));
203
204            publish_tokens.push(quote!(
205                #super_tokens #parent_topic_static.publish(#payload);
206            ));
207
208            super_tokens = quote!(#super_tokens super::);
209            last_topic = parent_topic;
210        }
211
212        publish_tokens
213    }
214}
215
216pub fn generate(ast: &Ast, _anaysis: &Analysis) -> proc_macro::TokenStream {
217    let mut subtopic_tracker = SubTopicTracker::new();
218
219    let tokens = codegen_subtopics(&ast.sub_topics, &mut subtopic_tracker);
220
221    quote! {
222        #(#tokens)*
223    }
224    .into()
225}