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}