Skip to main content

patterns_macros/
lib.rs

1use proc_macro::{TokenStream, TokenTree};
2
3enum Slot {
4    Name(String),
5    Text(String),
6}
7
8enum Want {
9    Name,
10    Text,
11    Names,
12}
13
14struct Shape {
15    call: &'static str,
16    usage: &'static str,
17    wants: &'static [Want],
18}
19
20const BECAUSE: Shape = Shape {
21    call: "because",
22    usage: "because!(ITEM, [CITE, ...], \"reason\")",
23    wants: &[Want::Name, Want::Names, Want::Text],
24};
25
26const DECIDED: Shape = Shape {
27    call: "decided",
28    usage: "decided!(TYPE, TRAIT, \"reason\")",
29    wants: &[Want::Name, Want::Name, Want::Text],
30};
31
32const SOURCE: Shape = Shape {
33    call: "source",
34    usage: "source!(ITEM, \"what it is\")",
35    wants: &[Want::Name, Want::Text],
36};
37
38const PROVISIONAL: Shape = Shape {
39    call: "provisional",
40    usage: "provisional!(ITEM, [CITE, ...], \"what would settle it\")",
41    wants: &[Want::Name, Want::Names, Want::Text],
42};
43
44const REJECTED: Shape = Shape {
45    call: "rejected",
46    usage: "rejected!(ITEM, \"alternative\", \"cost\")",
47    wants: &[Want::Name, Want::Text, Want::Text],
48};
49
50const SUPERSEDES: Shape = Shape {
51    call: "supersedes",
52    usage: "supersedes!(NEW, OLD, WHEN, \"why\")",
53    wants: &[Want::Name, Want::Name, Want::Name, Want::Text],
54};
55
56fn is_keyword(spelled: &str) -> bool {
57    matches!(spelled, "self" | "super" | "crate" | "Self")
58}
59
60fn complain(what: &str) -> TokenStream {
61    format!("compile_error!({:?});", what)
62        .parse()
63        .expect("complaint expands")
64}
65
66fn refuse(shape: &Shape, problem: &str) -> TokenStream {
67    complain(&format!("{}! {}; usage: {}", shape.call, problem, shape.usage))
68}
69
70fn is_text(lit: &str) -> bool {
71    lit.starts_with("\"") || lit.starts_with("r\"") || lit.starts_with("r#")
72}
73
74fn kind(text: bool) -> &'static str {
75    if text {
76        "a string"
77    } else {
78        "an item name"
79    }
80}
81
82fn split(input: TokenStream, shape: &Shape) -> Result<Vec<Slot>, TokenStream> {
83    let mut out: Vec<Slot> = Vec::new();
84    let mut open: Option<Slot> = None;
85    let mut angled = 0usize;
86    for t in input {
87        if angled > 0 {
88            match (&t, &mut open) {
89                (TokenTree::Punct(p), Some(Slot::Name(n))) if p.to_string() == ">" => {
90                    angled -= 1;
91                    n.push_str(&p.to_string());
92                }
93                (TokenTree::Punct(p), Some(Slot::Name(n))) if p.to_string() == "<" => {
94                    angled += 1;
95                    n.push_str(&p.to_string());
96                }
97                (TokenTree::Ident(i), Some(Slot::Name(n))) => n.push_str(&i.to_string()),
98                (TokenTree::Punct(p), Some(Slot::Name(n))) if p.to_string() == "," => n.push_str(&p.to_string()),
99                _ => return Err(refuse(shape, "takes item names, commas and string literals only")),
100            }
101            continue;
102        }
103        if let (TokenTree::Punct(p), Some(Slot::Name(n))) = (&t, &mut open) {
104            if p.to_string() == "<" {
105                angled += 1;
106                n.push_str(&p.to_string());
107                continue;
108            }
109        }
110        let next = match t {
111            TokenTree::Ident(i) => {
112                let spelled = i.to_string();
113                if is_keyword(&spelled) {
114                    return Err(refuse(shape, "takes item names, not keywords; self, super, crate and Self are paths"));
115                }
116                Slot::Name(spelled)
117            }
118            TokenTree::Literal(l) => {
119                let s = l.to_string();
120                if !is_text(&s) {
121                    return Err(refuse(
122                        shape,
123                        "takes string literals only; a number or a char is not a sentence",
124                    ));
125                }
126                Slot::Text(s)
127            }
128            TokenTree::Punct(p) if p.to_string() == "," => {
129                match open.take() {
130                    Some(s) => out.push(s),
131                    None => return Err(refuse(shape, "found an empty slot before a comma")),
132                }
133                continue;
134            }
135            TokenTree::Punct(p) if p.to_string() == ":" => {
136                return Err(refuse(
137                    shape,
138                    "takes item names, not paths; bring the item into scope and name it",
139                ))
140            }
141            _ => {
142                return Err(refuse(
143                    shape,
144                    "takes item names, commas and string literals only",
145                ))
146            }
147        };
148        if open.is_some() {
149            return Err(refuse(shape, "found two slots with no comma between them"));
150        }
151        open = Some(next);
152    }
153    if let Some(s) = open {
154        out.push(s);
155    }
156    Ok(out)
157}
158
159fn fit(slots: &[Slot], shape: &Shape) -> Option<TokenStream> {
160    let stretch = shape.wants.iter().any(|w| matches!(w, Want::Names));
161    let fixed = shape.wants.len() - usize::from(stretch);
162    if slots.len() < fixed {
163        return Some(refuse(shape, "is missing a slot"));
164    }
165    if !stretch && slots.len() > fixed {
166        return Some(refuse(shape, "has more slots than its shape takes"));
167    }
168    let extra = slots.len() - fixed;
169    let mut laid: Vec<bool> = Vec::new();
170    for w in shape.wants {
171        match w {
172            Want::Name => laid.push(false),
173            Want::Text => laid.push(true),
174            Want::Names => laid.extend(std::iter::repeat_n(false, extra)),
175        }
176    }
177    let last = slots.len() - 1;
178    let order = std::iter::once(last).chain(0..last);
179    for at in order {
180        let (slot, wants_text) = (&slots[at], laid[at]);
181        let got_text = matches!(slot, Slot::Text(_));
182        if got_text != wants_text {
183            return Some(refuse(
184                shape,
185                &format!(
186                    "slot {} wants {}, got {}",
187                    at + 1,
188                    kind(wants_text),
189                    kind(got_text)
190                ),
191            ));
192        }
193    }
194    None
195}
196
197fn parse(input: TokenStream, shape: &Shape) -> Result<Vec<Slot>, TokenStream> {
198    let slots = split(input, shape)?;
199    match fit(&slots, shape) {
200        Some(err) => Err(err),
201        None => Ok(slots),
202    }
203}
204
205fn spelled(slot: &Slot) -> &str {
206    match slot {
207        Slot::Name(s) => s,
208        Slot::Text(s) => s,
209    }
210}
211
212fn bare(name: &str) -> &str {
213    name.trim_start_matches("r#")
214}
215
216fn reasoned(input: TokenStream, shape: &Shape, tag: &str, from: &str) -> TokenStream {
217    let slots = match parse(input, shape) {
218        Ok(s) => s,
219        Err(e) => return e,
220    };
221    let item = spelled(&slots[0]);
222    let text = spelled(&slots[slots.len() - 1]);
223    let mut cite = String::new();
224    for (at, c) in slots[1..slots.len() - 1].iter().enumerate() {
225        let name = spelled(c);
226        cite.push_str(&format!(
227            "#[allow(unused_imports)] use self::{} as _; #[allow(non_upper_case_globals)] const __{}{}_{}: &str = stringify!({});",
228            name,
229            from,
230            at + 1,
231            bare(item),
232            name
233        ));
234    }
235    let rendered = format!(
236        "#[allow(non_upper_case_globals)] const __{}_{}: &str = {}; #[allow(unused_imports)] use self::{} as _; {}",
237        tag,
238        bare(item),
239        text,
240        item,
241        cite
242    );
243    rendered.parse().expect("reason expands")
244}
245
246#[proc_macro]
247pub fn because(input: TokenStream) -> TokenStream {
248    reasoned(input, &BECAUSE, "why", "from")
249}
250
251#[proc_macro]
252pub fn decided(input: TokenStream) -> TokenStream {
253    let slots = match parse(input, &DECIDED) {
254        Ok(s) => s,
255        Err(e) => return e,
256    };
257    let (ty, tr, why) = match slots.as_slice() {
258        [a, b, c] => (spelled(a), spelled(b), spelled(c)),
259        _ => return refuse(&DECIDED, "is missing a slot"),
260    };
261    let parts: Vec<&str> = tr.split(|c: char| !c.is_alphanumeric() && c.to_string() != "_").filter(|s| !s.is_empty()).collect();
262    let key: Vec<String> = parts.iter().map(|p| bare(p).to_string()).collect();
263    let mut uses = String::new();
264    for p in &parts {
265        uses.push_str(&format!("#[allow(unused_imports)] use self::{} as _; ", p));
266    }
267    let rendered = format!(
268        "#[allow(non_upper_case_globals)] const __why_{}_{}: &str = {}; #[allow(unused_imports)] use self::{} as _; {}",
269        bare(ty), key.join("_"), why, ty, uses
270    );
271    rendered.parse().expect("decided expands")
272}
273
274#[proc_macro]
275pub fn source(input: TokenStream) -> TokenStream {
276    reasoned(input, &SOURCE, "source", "sourcefrom")
277}
278
279#[proc_macro]
280pub fn provisional(input: TokenStream) -> TokenStream {
281    reasoned(input, &PROVISIONAL, "provisional", "provisionalfrom")
282}
283
284#[proc_macro]
285pub fn rejected(input: TokenStream) -> TokenStream {
286    let slots = match parse(input, &REJECTED) {
287        Ok(s) => s,
288        Err(e) => return e,
289    };
290    let rendered = match slots.as_slice() {
291        [item, alt, cost] => format!(
292            "const _: (&str, &str, &str) = (stringify!({}), {}, {}); #[allow(unused_imports)] use self::{} as _;",
293            spelled(item),
294            spelled(alt),
295            spelled(cost),
296            spelled(item)
297        ),
298        _ => return refuse(&REJECTED, "is missing a slot"),
299    };
300    rendered.parse().expect("rejected expands")
301}
302
303#[proc_macro]
304pub fn supersedes(input: TokenStream) -> TokenStream {
305    let slots = match parse(input, &SUPERSEDES) {
306        Ok(s) => s,
307        Err(e) => return e,
308    };
309    let now = spelled(&slots[0]);
310    let why = spelled(&slots[slots.len() - 1]);
311    let mut uses = String::new();
312    for (at, n) in slots[..slots.len() - 1].iter().enumerate() {
313        let name = spelled(n);
314        uses.push_str(&format!(
315            "#[allow(unused_imports)] use self::{} as _; #[allow(non_upper_case_globals)] const __sup{}_{}: &str = stringify!({});",
316            name,
317            at,
318            bare(now),
319            name
320        ));
321    }
322    let rendered = format!(
323        "#[allow(non_upper_case_globals)] const __was_{}: (&str, &str) = (stringify!({}), {}); {}",
324        bare(now),
325        spelled(&slots[1]),
326        why,
327        uses
328    );
329    rendered.parse().expect("supersedes expands")
330}
331
332#[proc_macro_derive(Record)]
333pub fn record(input: TokenStream) -> TokenStream {
334    let parsed = syn::parse_macro_input!(input as syn::DeriveInput);
335    let name = &parsed.ident;
336    let mut generics = parsed.generics.clone();
337    for param in generics.params.iter_mut() {
338        if let syn::GenericParam::Type(t) = param {
339            t.bounds.push(syn::parse_quote!(patterns::record::Fielded));
340        }
341    }
342    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
343    let body = match &parsed.data {
344        syn::Data::Struct(s) => match &s.fields {
345            syn::Fields::Named(named) => {
346                let names: Vec<&syn::Ident> = named.named.iter().map(|f| f.ident.as_ref().expect("named")).collect();
347                let texts: Vec<String> = names.iter().map(|n| n.to_string()).collect();
348                quote::quote! {
349                    fn field(&self) -> patterns::record::Field {
350                        patterns::record::Field::Table(vec![#((String::from(#texts), patterns::record::Fielded::field(&self.#names))),*])
351                    }
352                    fn refielded(field: &patterns::record::Field) -> Option<Self> {
353                        let table = patterns::record::table_of(field)?;
354                        Some(Self { #(#names: patterns::record::Fielded::refielded(patterns::record::looked_field(table, #texts))?),* })
355                    }
356                }
357            }
358            _ => return complain("Record derives on structs with named fields and on enums of unit variants only"),
359        },
360        syn::Data::Enum(e) => {
361            if e.variants.iter().any(|v| !matches!(v.fields, syn::Fields::Unit)) {
362                return complain("Record derives on enums of unit variants only");
363            }
364            let variants: Vec<&syn::Ident> = e.variants.iter().map(|v| &v.ident).collect();
365            let texts: Vec<String> = variants.iter().map(|v| v.to_string()).collect();
366            quote::quote! {
367                fn field(&self) -> patterns::record::Field {
368                    match self {
369                        #(Self::#variants => patterns::record::Field::Text(String::from(#texts))),*
370                    }
371                }
372                fn refielded(field: &patterns::record::Field) -> Option<Self> {
373                    let text = patterns::record::text_of_field(field)?;
374                    match text {
375                        #(#texts => Some(Self::#variants),)*
376                        _ => None,
377                    }
378                }
379            }
380        }
381        syn::Data::Union(_) => return complain("Record does not derive on unions"),
382    };
383    let out = quote::quote! {
384        impl #impl_generics patterns::record::Fielded for #name #ty_generics #where_clause {
385            #body
386        }
387    };
388    out.into()
389}