Skip to main content

elfo_macros_impl/
msg.rs

1use std::{char, collections::HashMap};
2
3use proc_macro2::Span;
4use quote::quote_spanned;
5use syn::{
6    parse_macro_input, spanned::Spanned, Arm, ExprMatch, Ident, Pat, PatIdent, PatWild, Path, Token,
7};
8
9use crate::errors::emit_error;
10
11#[derive(Debug)]
12struct MessageGroup {
13    kind: GroupKind,
14    arms: Vec<Arm>,
15}
16
17#[derive(Debug, Hash, PartialEq, Eq)]
18enum GroupKind {
19    // `msg @ Msg(..) => ...`
20    Regular(Path),
21    // `(msg @ Msg(..), token) => ...`
22    Request(Path),
23    // `_ =>`
24    // `msg =>`
25    Wild,
26}
27
28fn is_valid_token_ident(ident: &PatIdent) -> bool {
29    !ident.ident.to_string().starts_with('_')
30}
31
32fn is_type_ident(ident: &Ident) -> bool {
33    ident
34        .to_string()
35        .chars()
36        .next()
37        .is_some_and(char::is_uppercase)
38}
39
40fn extract_path_to_type(path: &Path) -> Path {
41    let mut ident_rev_it = path.segments.iter().rev();
42
43    // Handle enum variants:
44    // `some::Enum::Variant`
45    //        ^- must be uppercased
46    //
47    // Yep, it's crazy, but it seems to be a good assumption for now.
48    if let Some(prev) = ident_rev_it.nth(1) {
49        if is_type_ident(&prev.ident) {
50            let mut path = path.clone();
51            path.segments.pop().unwrap();
52
53            // Convert `Pair::Punctuated` to `Pair::End`.
54            let (last, _) = path.segments.pop().unwrap().into_tuple();
55            path.segments.push(last);
56            return path;
57        }
58    }
59
60    path.clone()
61}
62
63fn extract_kind(pat: &Pat) -> Result<GroupKind, &'static str> {
64    match pat {
65        Pat::Ident(pat) => match pat.subpat.as_ref() {
66            Some(sp) => extract_kind(&sp.1),
67            None if is_type_ident(&pat.ident) => {
68                Ok(GroupKind::Regular(Path::from(pat.ident.clone())))
69            }
70            None => Ok(GroupKind::Wild),
71        },
72        Pat::Lit(_) => Err("literal patterns are forbidden"),
73        Pat::Macro(_) => Err("macros in pattern position are forbidden"),
74        Pat::Or(pat) => pat
75            .cases
76            .iter()
77            .find_map(|pat| extract_kind(pat).ok())
78            .ok_or("cannot determine the message's type"),
79        Pat::Paren(_) => Err("parenthesized patterns are forbidden"),
80        Pat::Path(pat) => Ok(GroupKind::Regular(extract_path_to_type(&pat.path))),
81        Pat::Range(_) => Err("range patterns are forbidden"),
82        Pat::Reference(pat) => extract_kind(&pat.pat),
83        Pat::Rest(_) => Err("rest patterns are forbidden"),
84        Pat::Slice(_) => Err("slice patterns are forbidden"),
85        Pat::Struct(pat) => Ok(GroupKind::Regular(extract_path_to_type(&pat.path))),
86        Pat::Tuple(pat) => {
87            if pat.elems.len() != 2 {
88                return Err("invalid request pattern");
89            }
90
91            match pat.elems.last().unwrap() {
92                Pat::Ident(pat) => {
93                    if !is_valid_token_ident(pat) {
94                        emit_error!(
95                            pat.span(),
96                            "the token must be used, or call `drop(_)` explicitly"
97                        )
98                    }
99                }
100                _ => return Err("token must be identifier"),
101            }
102
103            match extract_kind(pat.elems.first().unwrap())? {
104                GroupKind::Regular(path) => Ok(GroupKind::Request(path)),
105                _ => Err("cannot determine the request's type"),
106            }
107        }
108        Pat::TupleStruct(pat) => Ok(GroupKind::Regular(extract_path_to_type(&pat.path))),
109        Pat::Type(_) => Err("type ascription patterns are forbidden"),
110        Pat::Wild(_) => Ok(GroupKind::Wild),
111        _ => Err("unknown tokens"),
112    }
113}
114
115fn is_likely_type(pat: &Pat) -> bool {
116    match pat {
117        Pat::Ident(i) if i.subpat.is_none() && is_type_ident(&i.ident) => true,
118        Pat::Path(p) if extract_path_to_type(&p.path) == p.path => true,
119        _ => false,
120    }
121}
122
123/// Detects `a @ A` and `a @ some::A` patterns.
124fn is_binding_with_type(ident: &PatIdent) -> bool {
125    ident
126        .subpat
127        .as_ref()
128        .is_some_and(|sp| is_likely_type(&sp.1))
129}
130
131fn refine_pat(pat: &mut Pat) {
132    match pat {
133        // `e @ Enum`
134        // `s @ Struct` (~ `s @ Struct { .. }`)
135        Pat::Ident(ident) if is_binding_with_type(ident) => {
136            ident.subpat = None;
137        }
138        // `(e @ SomeType, token)`
139        // `(SomeType, token)`
140        Pat::Tuple(pat) => {
141            // It's ok to use `assert_eq!` here because it must be already checked.
142            assert_eq!(pat.elems.len(), 2, "invalid request pattern");
143
144            match pat.elems.first_mut() {
145                Some(Pat::Ident(ident)) if is_binding_with_type(ident) => {
146                    ident.subpat = None;
147                }
148                Some(pat) if is_likely_type(pat) => {
149                    *pat = Pat::Wild(PatWild {
150                        attrs: Vec::new(),
151                        underscore_token: Token![_](pat.span()),
152                    });
153                }
154                _ => {}
155            }
156        }
157        // `SomeType => ...`
158        pat if is_likely_type(pat) => {
159            *pat = Pat::Wild(PatWild {
160                attrs: Vec::new(),
161                underscore_token: Token![_](pat.span()),
162            });
163        }
164        _ => {}
165    };
166}
167
168fn add_groups(groups: &mut Vec<MessageGroup>, mut arm: Arm) {
169    let mut add = |kind, arm: Arm| {
170        // println!("group {:?} {:#?}", kind, arm.pat);
171        match groups.iter_mut().find(|common| common.kind == kind) {
172            Some(common) => common.arms.push(arm),
173            None => groups.push(MessageGroup {
174                kind,
175                arms: vec![arm],
176            }),
177        }
178    };
179
180    if let Pat::Or(pat) = &arm.pat {
181        let mut map = HashMap::new();
182
183        for pat in &pat.cases {
184            let kind = match extract_kind(pat) {
185                Ok(kind) => kind,
186                Err(err) => {
187                    emit_error!(pat.span(), "{err}");
188                    continue;
189                }
190            };
191            let new_arm = map.entry(kind).or_insert_with(|| {
192                let mut arm = arm.clone();
193                if let Pat::Or(pat) = &mut arm.pat {
194                    pat.cases.clear();
195                }
196                arm
197            });
198
199            if let Pat::Or(new_pat) = &mut new_arm.pat {
200                let mut old_pat = pat.clone();
201                refine_pat(&mut old_pat);
202                new_pat.cases.push(old_pat);
203            }
204        }
205
206        for (kind, arm) in map {
207            add(kind, arm);
208        }
209    } else {
210        let kind = match extract_kind(&arm.pat) {
211            Ok(kind) => kind,
212            Err(err) => return emit_error!(arm.pat.span(), "{err}"),
213        };
214        refine_pat(&mut arm.pat);
215        add(kind, arm);
216    }
217}
218
219/// Implements the `msg!` macro.
220pub fn msg_impl(input: proc_macro::TokenStream, path_to_elfo: Path) -> proc_macro::TokenStream {
221    let crate_ = path_to_elfo;
222    let mixed_site = Span::mixed_site();
223    let input = parse_macro_input!(input as ExprMatch);
224    let mut groups = Vec::<MessageGroup>::with_capacity(input.arms.len());
225
226    for arm in input.arms.into_iter() {
227        add_groups(&mut groups, arm);
228    }
229
230    // println!(">>> HERE {:#?}", groups);
231
232    let groups = groups
233        .iter()
234        .map(|group| match (&group.kind, &group.arms[..]) {
235            // Specify the span for better error localization:
236            // - used the regular syntax while the request one is expected
237            // - unexhaustive match
238            (GroupKind::Regular(path), arms) => quote_spanned! {mixed_site=>
239                else if type_id == <#path as #crate_::Message>::_type_id() {
240                    // Ensure it's not a request, or a request but only in a borrowed context.
241                    // We cannot use `static_assertions` here because it wraps the check into
242                    // a closure that forbids us to use generic `msg!`: (`msg!(match e { M => .. })`).
243                    {
244                        trait MustBeRegularNotRequest<A, E> { fn test(_: &E) {} }
245                        impl<E, M> MustBeRegularNotRequest<(), E> for M {}
246                        struct Invalid;
247                        impl<E: internal::EnvelopeOwned, M: #crate_::Request>
248                            MustBeRegularNotRequest<Invalid, E> for M {}
249                        <#path as MustBeRegularNotRequest<_, _>>::test(&envelope)
250                    }
251
252                    #[allow(unknown_lints, clippy::blocks_in_conditions)]
253                    match {
254                        // Support both owned and borrowed contexts, relying on the type inference.
255                        #[allow(unused_imports)]
256                        use internal::{EnvelopeOwned as _, EnvelopeBorrowed as _};
257                        unsafe { envelope.unpack_regular_unchecked::<#path>() }
258                    } {
259                        #(#arms)*
260                    }
261                }
262            },
263            (GroupKind::Request(path), arms) => quote_spanned! {mixed_site=>
264                else if type_id == <#path as #crate_::Message>::_type_id() {
265                    // Ensure it's a request. We cannot use `static_assertions` here
266                    // because it wraps the check into a closure that forbids us to
267                    // use generic `msg!`: (`msg!(match e { (R, token) => .. })`).
268                    {
269                        fn must_be_request<R: #crate_::Request>() {}
270                        must_be_request::<#path>();
271                    }
272
273                    #[allow(unknown_lints, clippy::blocks_in_conditions)]
274                    match {
275                        // Only the owned context is supported.
276                        #[allow(unused_imports)]
277                        use internal::EnvelopeOwned as _;
278                        unsafe { envelope.unpack_request_unchecked::<#path>() }
279                    } {
280                        #(#arms)*
281                    }
282                }
283            },
284            (GroupKind::Wild, arms) => {
285                let mut arms_iter = arms.iter();
286                let arm = arms_iter.next().unwrap();
287
288                let expanded = quote_spanned! {mixed_site=>
289                    else {
290                        match envelope { #arm }
291                    }
292                };
293
294                for arm in arms_iter {
295                    emit_error!(arm.pat.span(), "this branch will never be matched");
296                }
297
298                expanded
299            }
300        });
301
302    let match_expr = input.expr;
303
304    // TODO: propagate `input.attrs`?
305    let expanded = quote_spanned!(mixed_site=> {
306        use #crate_::_priv as internal;
307        let envelope = #match_expr;
308        let type_id = envelope.type_id();
309        #[allow(clippy::suspicious_else_formatting)]
310        if false { unreachable!(); }
311        #(#groups)*
312    });
313
314    // Errors must be checked after expansion, otherwise some errors can be lost.
315    if let Some(errors) = crate::errors::into_tokens() {
316        quote_spanned!(mixed_site=> { #errors #expanded }).into()
317    } else {
318        expanded.into()
319    }
320}