phantom_fields/
lib.rs

1// Note(Lokathor): this extern crate is necessary even in 2018 for whatever
2// reason that I'm sure is stupid.
3extern crate proc_macro;
4
5use core::str::FromStr;
6use proc_macro::TokenStream;
7use proc_macro2::Span;
8use quote::quote;
9use syn::{
10  parse::{Parse, ParseStream, Result},
11  parse_macro_input, Attribute, Error, Ident, LitInt, Token, TypePath,
12};
13
14// Phantom Fields
15
16enum PhantomEntry {
17  Enum {
18    attributes: Vec<Attribute>,
19    name: String,
20    start: u64,
21    end: u64,
22    enum_type: Ident,
23    variant_list: Vec<String>,
24  },
25  Integer {
26    attributes: Vec<Attribute>,
27    name: String,
28    start: u64,
29    end: u64,
30  },
31  Bool {
32    attributes: Vec<Attribute>,
33    name: String,
34    bit: u64,
35  },
36  Const {
37    attributes: Vec<Attribute>,
38    name: String,
39    const_ident: Ident,
40  },
41}
42
43struct PhantomFields {
44  self_member_type: TypePath,
45  entries: Vec<PhantomEntry>,
46}
47
48impl Parse for PhantomFields {
49  fn parse(input: ParseStream) -> Result<Self> {
50    let _ = input.parse::<Token![self]>()?;
51    let _ = input.parse::<Token![.]>()?;
52    let lit = input.parse::<LitInt>()?;
53    if lit.value() != 0 {
54      return Err(Error::new(lit.span(), "Currently only self.0 is supported"));
55    }
56    let _ = input.parse::<Token![:]>()?;
57    let self_member_type: TypePath = input.parse::<TypePath>()?;
58    let _ = input.parse::<Token![,]>()?;
59    //
60    let mut entries: Vec<PhantomEntry> = vec![];
61    'entry_loop: loop {
62      if input.is_empty() {
63        break;
64      }
65      let attributes = input.call(Attribute::parse_outer)?;
66      let name = input.parse::<Ident>()?.to_string();
67      let _ = input.parse::<Token![:]>()?;
68      let lookahead_int_or_ident = input.lookahead1();
69      if lookahead_int_or_ident.peek(LitInt) {
70        let start = input.parse::<LitInt>()?.value();
71        let lookahead_bool_or_span = input.lookahead1();
72        if lookahead_bool_or_span.peek(Token![,]) {
73          // bool entry
74          entries.push(PhantomEntry::Bool {
75            attributes,
76            name,
77            bit: start,
78          });
79          let _ = input.parse::<Token![,]>()?;
80          continue 'entry_loop;
81        } else if lookahead_bool_or_span.peek(Token![-]) {
82          // spanning entry
83          let _ = input.parse::<Token![-]>()?;
84          let end = input.parse::<LitInt>()?.value();
85          let lookahead = input.lookahead1();
86          if lookahead.peek(Token![=]) {
87            // enum span
88            let _ = input.parse::<Token![=]>()?;
89            let enum_type = input.parse::<Ident>()?;
90            let mut variant_list = vec![];
91            let _ = input.parse::<Token![<]>()?;
92            'variant_gather_loop: loop {
93              variant_list.push(input.parse::<Ident>()?.to_string());
94              let lookahead = input.lookahead1();
95              if lookahead.peek(Token![>]) {
96                // end of list
97                let _ = input.parse::<Token![>]>()?;
98                break 'variant_gather_loop;
99              } else if lookahead.peek(Token![,]) {
100                // more to gather
101                let _ = input.parse::<Token![,]>()?;
102                continue 'variant_gather_loop;
103              } else {
104                return Err(lookahead.error());
105              }
106            }
107            entries.push(PhantomEntry::Enum {
108              attributes,
109              name,
110              start,
111              end,
112              enum_type,
113              variant_list,
114            });
115            let _ = input.parse::<Token![,]>()?;
116            continue 'entry_loop;
117          } else if lookahead.peek(Token![,]) {
118            // int span
119            entries.push(PhantomEntry::Integer {
120              attributes,
121              name,
122              start,
123              end,
124            });
125            let _ = input.parse::<Token![,]>()?;
126            continue 'entry_loop;
127          } else {
128            return Err(lookahead.error());
129          }
130        } else {
131          return Err(lookahead_bool_or_span.error());
132        }
133      } else if lookahead_int_or_ident.peek(Ident) {
134        let const_ident = input.parse::<Ident>()?;
135        // constant literal entry
136        entries.push(PhantomEntry::Const {
137          attributes,
138          name,
139          const_ident,
140        });
141        let _ = input.parse::<Token![,]>()?;
142        continue 'entry_loop;
143      } else {
144        return Err(lookahead_int_or_ident.error());
145      }
146    }
147    Ok(PhantomFields { self_member_type, entries })
148  }
149}
150
151/// Declares a struct to have "phantom" fields. Use within an `impl` block.
152///
153/// At the moment, this only supports tuple structs and only targets the `0`
154/// field of said structs. It's intended for use with newtype'd unsigned
155/// integers of various sizes.
156///
157/// The proc-macro's grammar is fairly simple once you get the hang of it but
158/// there's several possibilities you can declare.
159///
160/// * `self.0: [TYPE],` to declare the type of the `self.0` field.
161/// * Then you declare 0 or more phantom fields:
162///   * Each phantom field can have attributes, which is mostly intended for
163///     allowing rustdoc.
164///   * Then the name of the field, followed by `:`
165///   * Then you describe "where" the field is with a `,` at the end:
166///     * A single integer makes a bool field at that bit.
167///     * An `a-b` integer span makes an integer field in that inclusive bit
168///       range.
169///     * An `a-b=ENUM<TAG1,...,TAG_N>` location makes an enum field of the type
170///       given that can be any of the tags you specify. In the decoding method
171///       an `_ => unreachable!()` branch is placed after all tags, so if you
172///       specify less tags than possible bit patterns (eg: only 3 tags for a
173///       2-bit field) your code can potentially panic.
174///     * An identifier will assume that the identifier is already a const
175///       declared somewhere in scope, and use that as the bit location of a
176///       bool-typed field.
177/// * Enum and integer phantom fields also get a `FOO_MASK` const added to the
178///   struct type for each field named `foo`. This const is what takes on the
179///   attributes you specified for that phantom field.
180/// * Bool phantom fields get a `FOO_BIT` const added to the struct for each
181///   field named foo. This gets the attributes of the field.
182/// * Const phantom fields don't get added to the struct, they're already a
183///   const somewhere after all.
184///
185/// Once this is all set up you'll of course want to use the phantom fields:
186///
187/// * phantom fields can be _read_ using their name as a the method name,
188///   similar to normal rust "getters".
189/// * phantom fields can be _replaced_ "builder style" by taking `self` and
190///   giving a new value using `with_field`. This is `const`, so you can use it
191///   to declare a const that has some particular setting combination.
192///
193/// I think it makes more sense if you see it in action. Here's an extended
194/// example of all the situations supported.
195///
196/// ```rust,no_run
197/// use phantom_fields::phantom_fields;
198///
199/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
200/// #[repr(u32)]
201/// pub enum DisplayMode {
202///   Good = 0,
203///   Bad = 1,
204///   Ugly = 2,
205/// }
206///
207/// pub const CONST_TEST_VALUE: u32 = 1 << 13;
208///
209/// #[derive(Debug, Default, Clone, Copy)]
210/// #[repr(transparent)]
211/// pub struct PhantomFieldsDemo(u32);
212///
213/// impl PhantomFieldsDemo {
214///   phantom_fields! {
215///     self.0: u32,
216///     /// enum_example docs
217///     enum_example: 0-2=DisplayMode<Good, Bad, Ugly>,
218///     bool_example: 3,
219///     /// This gives us a 2-bit field
220///     int_example: 6-7,
221///     const_example: CONST_TEST_VALUE,
222///   }
223/// }
224/// ```
225#[proc_macro]
226pub fn phantom_fields(input: TokenStream) -> TokenStream {
227  let PhantomFields { self_member_type, entries } = parse_macro_input!(input as PhantomFields);
228
229  let mut out_text = String::new();
230
231  for entry in entries.into_iter() {
232    match entry {
233      PhantomEntry::Enum {
234        attributes,
235        name,
236        start,
237        end,
238        enum_type,
239        variant_list,
240      } => {
241        for attribute in attributes.into_iter() {
242          out_text.push_str(&format!("{}\n", TokenStream::from(quote! { #attribute })));
243        }
244        let mask_name = Ident::new(&format!("{}_MASK", name.to_uppercase()), Span::call_site());
245        let read_name = Ident::new(&name.clone(), Span::call_site());
246        let with_name = Ident::new(&format!("with_{}", name), Span::call_site());
247        let width = (end - start) + 1;
248        out_text.push_str(&format!(
249          "{}\n",
250          TokenStream::from(quote! {
251            #[allow(clippy::identity_op)]
252            pub const #mask_name: #self_member_type = ((1<<(#width))-1) << #start;
253
254            #[allow(missing_docs)]
255            pub fn #read_name(self) -> #enum_type
256          })
257        ));
258        out_text.push('{');
259        out_text.push_str(&format!(
260          "{}\n",
261          TokenStream::from(quote! {
262            match (self.0 & Self::#mask_name) >> #start
263          })
264        ));
265        out_text.push('{');
266        let enum_type_string = enum_type.to_string();
267        for (i, variant) in variant_list.iter().enumerate() {
268          out_text.push_str(&format!("{} => {}::{},\n", i, enum_type_string, variant));
269        }
270        out_text.push_str("_ => unreachable!(),");
271        out_text.push_str("} }\n");
272        out_text.push_str(&format!(
273          "{}\n",
274          TokenStream::from(quote! {
275            #[allow(missing_docs)]
276            pub const fn #with_name(self, #read_name: #enum_type) -> Self {
277              Self((self.0 & !Self::#mask_name) | (((#read_name as #self_member_type) << #start) & Self::#mask_name))
278            }
279          })
280        ));
281      }
282      PhantomEntry::Integer {
283        attributes,
284        name,
285        start,
286        end,
287      } => {
288        for attribute in attributes.into_iter() {
289          out_text.push_str(&format!("{}\n", TokenStream::from(quote! { #attribute })));
290        }
291        let mask_name = Ident::new(&format!("{}_MASK", name.to_uppercase()), Span::call_site());
292        let read_name = Ident::new(&name.clone(), Span::call_site());
293        let with_name = Ident::new(&format!("with_{}", name), Span::call_site());
294        let width = (end - start) + 1;
295        out_text.push_str(&format!(
296          "{}\n",
297          TokenStream::from(quote! {
298            #[allow(clippy::identity_op)]
299            pub const #mask_name: #self_member_type = ((1<<(#width))-1) << #start;
300
301            #[allow(missing_docs)]
302            pub const fn #read_name(self) -> #self_member_type {
303              (self.0 & Self::#mask_name) >> #start
304            }
305
306            #[allow(missing_docs)]
307            pub const fn #with_name(self, #read_name: #self_member_type) -> Self {
308              Self((self.0 & !Self::#mask_name) | ((#read_name << #start) & Self::#mask_name))
309            }
310          })
311        ));
312      }
313      PhantomEntry::Bool { attributes, name, bit } => {
314        for attribute in attributes.into_iter() {
315          out_text.push_str(&format!("{}\n", TokenStream::from(quote! { #attribute })));
316        }
317        let const_name = Ident::new(&format!("{}_BIT", name.to_uppercase()), Span::call_site());
318        let read_name = Ident::new(&name.clone(), Span::call_site());
319        let with_name = Ident::new(&format!("with_{}", name), Span::call_site());
320        out_text.push_str(&format!(
321          "{}\n",
322          TokenStream::from(quote! {
323            #[allow(clippy::identity_op)]
324            pub const #const_name: #self_member_type = 1 << #bit;
325
326            #[allow(missing_docs)]
327            pub const fn #read_name(self) -> bool {
328              (self.0 & Self::#const_name) != 0
329            }
330
331            // https://graphics.stanford.edu/~seander/bithacks.html#ConditionalSetOrClearBitsWithoutBranching
332            #[allow(missing_docs)]
333            pub const fn #with_name(self, bit: bool) -> Self {
334              Self(self.0 ^ (((#self_member_type::wrapping_sub(0, bit as #self_member_type) ^ self.0) & Self::#const_name)))
335            }
336          })
337        ));
338      }
339      PhantomEntry::Const {
340        attributes,
341        name,
342        const_ident,
343      } => {
344        for attribute in attributes.into_iter() {
345          out_text.push_str(&format!("{}\n", TokenStream::from(quote! { #attribute })));
346        }
347        let read_name = Ident::new(&name.clone(), Span::call_site());
348        let with_name = Ident::new(&format!("with_{}", name), Span::call_site());
349        out_text.push_str(&format!(
350          "{}\n",
351          TokenStream::from(quote! {
352            #[allow(missing_docs)]
353            pub const fn #read_name(self) -> bool {
354              (self.0 & #const_ident) != 0
355            }
356
357            // https://graphics.stanford.edu/~seander/bithacks.html#ConditionalSetOrClearBitsWithoutBranching
358            #[allow(missing_docs)]
359            pub const fn #with_name(self, bit: bool) -> Self {
360              Self(self.0 ^ (((#self_member_type::wrapping_sub(0, bit as #self_member_type) ^ self.0) & #const_ident as #self_member_type)))
361            }
362          })
363        ));
364      }
365    };
366  }
367
368  TokenStream::from_str(&out_text).map_err(|e| panic!("{:?}", e)).unwrap()
369}