Skip to main content

enum_info/
lib.rs

1// enum-info
2// 
3// Written by Calin Z. Baenen, 2026/07/19
4
5//! `enum-info` is a crate to generate an enum `impl` which can tell you the
6//!  number of – and, in most cases, list the – variants in an enum.
7
8use proc_macro::{
9	TokenStream,
10	Delimiter,
11	TokenTree,
12	Literal,
13	Spacing,
14	Group,
15	Ident,
16	Punct,
17	Span
18};
19
20use core::cmp::{PartialOrd, PartialEq, Ordering, Ord, Eq};
21use core::num::NonZeroUsize;
22use std::vec::Vec;
23
24
25
26
27
28#[allow(unused)]
29trait SubposData<S:Subpos> {
30	fn position(&self) -> &S;
31	
32	fn data(&self) -> &S::Data;
33}
34
35
36
37trait Subpos:PartialEq {
38	type Data:PartialEq;
39	
40	const ASSOCIATED_STAGE:LexicalPosStage;
41}
42
43impl Subpos for () {
44	type Data = ();
45	
46	const ASSOCIATED_STAGE:LexicalPosStage = LexicalPosStage::Start;
47}
48
49
50
51
52
53#[derive(PartialEq, Clone, Copy, Eq)]
54#[repr(u8)]
55enum GenericParamType {
56	Type     = 0,
57	Const    = 1,
58	Lifetime = 2
59}
60
61impl GenericParamType {
62	#[inline(always)] pub fn prefix(&self) -> Option<TokenTree> {
63		match self {
64			Self::Type => { None }
65			
66			Self::Const => { Some( TokenTree::Ident(Ident::new("const", Span::call_site())) ) }
67			
68			Self::Lifetime => { Some( TokenTree::Punct(Punct::new('\'', Spacing::Joint)) ) }
69		}
70	}
71}
72
73use GenericParamType::*;
74
75
76
77/// The position inside of a generic parameter.
78#[derive(PartialOrd, PartialEq, Clone, Copy, Ord, Eq)]
79enum GenericParamPos {
80	Name              = 0,
81	Bounds            = 1,
82	DefaultAssignment = 2
83}
84
85impl Subpos for GenericParamPos {
86	type Data = NonZeroUsize;
87	
88	const ASSOCIATED_STAGE:LexicalPosStage = LexicalPosStage::Generics;
89}
90
91
92
93#[derive(PartialOrd, PartialEq,Clone, Debug, Copy, Ord, Eq)]
94#[repr(usize)]
95enum LexicalPosStage {
96	/// Indicates anything before, and up to, the `enum` keyword.
97	Start = 0,
98	/// Indicates the position of the name of the enum.
99	Name = 1,
100	/// Indicates the position directly following the name but before the generics and enum body.
101	AfterName = 2,
102	/// Indicates the position between the outermost `<`/`>`.
103	/// 
104	/// If `depth` is greater than `1`, `pos` should not be [`GenericParamPos::Name`].
105	Generics = 3,
106	/// Indicates the position directly following the generics.
107	AfterGenerics = 4,
108	/// Indicates the position after the `where` keyword but before the enum body.
109	WhereClause = 5,
110	/// Indicates the position inside an enum body.
111	EnumBody = 6
112}
113
114
115
116#[derive(Clone, Copy)]
117union LexicalPosData {
118	enum_body:(EnumBodyPos, <EnumBodyPos as Subpos>::Data),
119	generics:(GenericParamPos, <GenericParamPos as Subpos>::Data),
120	usize:usize,
121	_marker:()
122}
123
124impl SubposData<EnumBodyPos> for LexicalPosData {
125	fn position(&self) -> &EnumBodyPos { unsafe { &self.enum_body.0 } }
126	
127	fn data(&self) -> &<EnumBodyPos as Subpos>::Data { unsafe { &self.enum_body.1 } }
128}
129
130impl SubposData<GenericParamPos> for LexicalPosData {
131	fn position(&self) -> &GenericParamPos { unsafe { &self.generics.0 } }
132	
133	fn data(&self) -> &<GenericParamPos as Subpos>::Data { unsafe { &self.generics.1 } }
134}
135
136
137
138#[derive(Clone)]
139struct GenericParam {
140	pub default_item:Option<TokenStream>,
141	pub param_type:GenericParamType,
142	pub bounds:Option<TokenStream>,
143	pub name:Option<Ident>
144}
145
146impl GenericParam {
147	pub const DEFAULT:Self = Self {default_item: None, param_type: GenericParamType::Type, bounds: None, name: None};
148}
149
150
151
152#[derive(PartialEq, Clone, Copy, Eq)]
153enum EnumBodyPos {
154	/// Indicates the position of an enum variant.
155	Variant,
156	/// Indicates the position after an enum variant's
157	///  name but before the `=`.
158	AfterVariant,
159	/// Indicates the position following the equals
160	///  sign in an enum variant declaration.
161	Definition
162}
163
164impl Subpos for EnumBodyPos {
165	type Data = ();
166	
167	const ASSOCIATED_STAGE:LexicalPosStage = LexicalPosStage::EnumBody;
168}
169
170
171
172/// The expected lexical position of a token.
173#[derive(Clone, Copy)]
174struct LexicalPos {
175	pub stage:LexicalPosStage,
176	pub data:LexicalPosData
177}
178
179impl LexicalPos {
180	#[inline(always)] pub const fn after_generics() -> Self {
181		Self {
182			stage: LexicalPosStage::AfterGenerics,
183			data: LexicalPosData { _marker: () }
184		}
185	}
186	
187	#[inline(always)] pub const fn where_clause() -> Self {
188		Self {
189			stage: LexicalPosStage::WhereClause,
190			data: LexicalPosData { usize: 0 }
191		}
192	}
193	
194	#[inline(always)] pub const fn after_name() -> Self {
195		Self {
196			stage: LexicalPosStage::AfterName,
197			data: LexicalPosData { _marker: () }
198		}
199	}
200	
201	#[inline(always)] pub const fn enum_body(pos:EnumBodyPos) -> Self {
202		Self {
203			stage: LexicalPosStage::EnumBody,
204			data: LexicalPosData { enum_body: (pos, ()) }
205		}
206	}
207	
208	#[inline(always)] pub const fn generics(depth:<GenericParamPos as Subpos>::Data, pos:GenericParamPos) -> Self {
209		Self {
210			stage: LexicalPosStage::Generics,
211			data: LexicalPosData { generics: (pos, depth) }
212		}
213	}
214	
215	#[inline(always)] pub const fn start() -> Self {
216		Self {
217			stage: LexicalPosStage::Start,
218			data: LexicalPosData { _marker: () }
219		}
220	}
221	
222	#[inline(always)] pub const fn name() -> Self {
223		Self {
224			stage: LexicalPosStage::Name,
225			data: LexicalPosData { _marker: () }
226		}
227	}
228	
229	/// Returns whether [`LexicalPosStage::EnumBody`] is a valid next position.
230	#[inline(always)] pub const fn enum_body_can_follow(&self) -> bool {
231		   self.stage as usize == LexicalPosStage::AfterName       as usize
232		|| self.stage as usize == LexicalPosStage::AfterGenerics   as usize
233		|| (
234		       self.stage as usize == LexicalPosStage::WhereClause as usize
235		    && unsafe { self.data.usize } == 0
236		)
237	}
238	
239	pub fn after_substate<S>(&self, substate:S) -> bool where LexicalPosData:SubposData<S>, S:Subpos+Ord {
240		   self.stage == S::ASSOCIATED_STAGE
241		&& <LexicalPosData as SubposData<S>>::position(&self.data).gt(&substate)
242	}
243	
244	/// Returns how deep into generics the lexical position is.
245	/// 
246	/// If the lexical position is not inside generics, `0` is returned.
247	#[inline(always)] pub const fn generics_depth(&self) -> usize {
248		if self.stage as usize != LexicalPosStage::Generics as usize { return 0; }
249		unsafe {
250			self.data.generics.1.get()
251		}
252	}
253	
254	pub fn in_substate<S>(&self, substate:S) -> bool where LexicalPosData:SubposData<S>, S:Subpos {
255		   self.stage == S::ASSOCIATED_STAGE
256		&& <LexicalPosData as SubposData<S>>::position(&self.data).eq(&substate)
257	}
258}
259
260impl PartialOrd<LexicalPosStage> for LexicalPos {
261	#[inline(always)] fn partial_cmp(&self, rhs:&LexicalPosStage) -> Option<Ordering> { Some(self.stage.cmp(rhs)) }
262}
263
264impl PartialOrd for LexicalPos {
265	/// Compares two [`LexicalPos`]es on the basis of which stage they are in.
266	#[inline(always)] fn partial_cmp(&self, rhs:&Self) -> Option<Ordering> { Some(self.cmp(rhs)) }
267}
268
269impl PartialEq<LexicalPosStage> for LexicalPos {
270	#[inline(always)] fn eq(&self, rhs:&LexicalPosStage) -> bool { self.stage.eq(rhs) }
271}
272
273impl PartialEq for LexicalPos {
274	#[inline] fn eq(&self, rhs:&Self) -> bool {
275		match (self.stage, rhs.stage) {
276			(LexicalPosStage::Generics, LexicalPosStage::Generics) => { unsafe {
277				self.data.generics == rhs.data.generics
278			} }
279			
280			(LexicalPosStage::EnumBody, LexicalPosStage::EnumBody) => { unsafe {
281				self.data.enum_body == rhs.data.enum_body
282			} }
283			
284			(x, y) => { x == y }
285		}
286	}
287}
288
289impl Ord for LexicalPos {
290	/// Compares two [`LexicalPos`]es on the basis of which stage they are in.
291	#[inline(always)] fn cmp(&self, rhs:&Self) -> Ordering { self.stage.cmp(&rhs.stage) }
292}
293
294impl Eq for LexicalPos {}
295
296
297
298
299
300/// Checks if two [`Option<Punct>`]s are equal based on their [`char`] and
301///  [`Spacing`] values.
302/// 
303/// If either value is [`Option::None`], then `false` is returned, regardless
304///  of whether both inputs are [`Option::None`].
305#[inline] fn cmp_punct(punct_a:Option<Punct>, punct_b:Option<Punct>) -> bool {
306	let (Some(a), Some(b)) = (punct_a, punct_b) else { return false; };
307	
308	   a.as_char() == b.as_char()
309	&& a.spacing() == b.spacing()
310}
311
312
313
314/// Generates an item `impl` with a guaranteed `variant_count` method and an
315///  associated constant, `VARIANTS`, for enums whose variants are all unit-like.
316/// 
317/// `variant_count` is a constant function which returns the number of variants
318///  the enum has.
319/// 
320/// `VARIANTS` is an array whose elements consist of each enum variant in order.
321#[proc_macro_attribute]
322pub fn enum_info(_attr:TokenStream, item:TokenStream) -> TokenStream {
323	let mut current_generic_param = GenericParam::DEFAULT;
324	let mut prev_tok_as_punct = None;
325	let mut generic_params = Vec::<GenericParam>::new();
326	let mut variant_names = Vec::<Ident>::new();
327	let mut where_clause = None;
328	let mut variant_ct = 0;
329	let mut all_unit = true;
330	let mut position = LexicalPos::start();
331	let mut output = item.clone();
332	let mut name = String::new();
333	
334	for token in item {
335		let mut generic_param_update = false;
336		let mut tok_as_punct = None;
337		let mut appendage = None;
338		
339		// Loop over the tokens of the outer item's content.
340		match token {
341			TokenTree::Group(g) if position.enum_body_can_follow() && !cmp_punct(prev_tok_as_punct.clone(), Some(Punct::new('!', Spacing::Alone))) && g.delimiter() == Delimiter::Brace => {
342				position = LexicalPos::enum_body(EnumBodyPos::Variant);
343				for subtoken in g.stream() {
344					match subtoken {
345						TokenTree::Group(_) if position.in_substate(EnumBodyPos::AfterVariant) => {
346							if !all_unit { continue; }
347							
348							variant_names.clear();
349							variant_names.shrink_to(0);
350							all_unit = false;
351						}
352						
353						TokenTree::Ident(i) if position.in_substate(EnumBodyPos::Variant) => {
354							position.data.enum_body.0 = EnumBodyPos::AfterVariant;
355							variant_ct += 1;
356							
357							if all_unit { variant_names.push(i); }
358						}
359						
360						TokenTree::Punct(p) => {
361							match p.as_char() {
362								'=' => {
363									position.data.enum_body.0 = EnumBodyPos::Definition;
364								}
365								
366								',' => {
367									position.data.enum_body.0 = EnumBodyPos::Variant;
368								}
369								
370								_ => {}
371							}
372						}
373						
374						_ => {}
375					}
376				}
377			}
378			
379			TokenTree::Group(g) if (position.after_substate(GenericParamPos::Name) || position == LexicalPosStage::WhereClause) => {
380				appendage = Some(TokenTree::Group(g));
381			}
382			
383			TokenTree::Ident(i) if position == LexicalPosStage::Name => {
384				name = i.to_string();
385				position = LexicalPos::after_name();
386				continue;
387			}
388			
389			TokenTree::Ident(i) => {
390				match i.to_string().as_str() {
391					"enum" if position == LexicalPosStage::Start => {
392						position = LexicalPos::name();
393					},
394					
395					// Const generics.
396					"const" if position.generics_depth() == 1
397					        && current_generic_param.name.is_none()
398					        && current_generic_param.param_type == Type => { current_generic_param.param_type = Const; },
399					
400					// Where clause.
401					"where" if position == LexicalPosStage::AfterName || position == LexicalPosStage::AfterGenerics => {
402						position = LexicalPos::where_clause();
403					}
404					
405					_ if position.in_substate(GenericParamPos::Name) && current_generic_param.name.is_none() => {
406						current_generic_param.name = Some(i);
407					},
408					
409					_ if (position.after_substate(GenericParamPos::Name) || position == LexicalPosStage::WhereClause) => {
410						appendage = Some(TokenTree::Ident(i));
411					}
412					
413					_ => {}
414				}
415			}
416			
417			TokenTree::Punct(p) => {
418				let c = p.as_char();
419				tok_as_punct = Some(p.clone());
420				
421				match c {
422					// Lifetimes.
423					'\'' if position.in_substate(GenericParamPos::Name)
424					     && current_generic_param.name.is_none()
425					     && current_generic_param.param_type == Type => { current_generic_param.param_type = Lifetime; }
426					
427					// Open generics.
428					'<' => {
429						if position == LexicalPosStage::AfterName {
430							position = LexicalPos::generics(unsafe { NonZeroUsize::new_unchecked(1) }, GenericParamPos::Name);
431							continue;
432						} else if position.after_substate(GenericParamPos::Name) {
433							appendage = Some(TokenTree::Punct(p));
434							position.data.generics.1 = unsafe { position.data.generics }.1.saturating_add(1);
435						} else if position == LexicalPosStage::WhereClause {
436							unsafe { position.data.usize += 1; }
437							appendage = Some(TokenTree::Punct(p));
438						}
439					}
440					
441					// Close generics.
442					'>' if position == LexicalPosStage::Generics && !cmp_punct(prev_tok_as_punct.clone(), Some(Punct::new('-', Spacing::Joint))) => 'close_generics: {
443						if unsafe { position.data.generics }.1.get() == 1 {
444							generic_param_update = true;
445							position = LexicalPos::after_generics();
446							
447							break 'close_generics;
448						}
449						
450						position.data.generics.1 =
451							unsafe { NonZeroUsize::new_unchecked(position.data.generics.1.get().saturating_sub(1)) };
452						appendage = Some(TokenTree::Punct(p));
453					}
454					
455					// Close generics (in `where` clauses).
456					'>' if position == LexicalPosStage::WhereClause && !cmp_punct(prev_tok_as_punct.clone(), Some(Punct::new('-', Spacing::Joint))) => 'close_generics: { unsafe {
457						appendage = Some(TokenTree::Punct(p));
458						
459						if position.data.usize < 1 { break 'close_generics; }
460						position.data.usize -= 1;
461					} }
462					
463					// Move from the name part of generics to the bounds.
464					':' if position.in_substate(GenericParamPos::Name) => {
465						position.data.generics.0 = GenericParamPos::Bounds;
466					}
467					
468					// Capture default values for generic parameters.
469					'=' if !position.after_substate(GenericParamPos::Bounds) && position == LexicalPosStage::Generics => {
470						position.data.generics.0 = GenericParamPos::DefaultAssignment;
471					}
472					
473					// Push the current generic parameter.
474					',' if position.generics_depth() == 1 => {
475						position.data.generics.0 = GenericParamPos::Name;
476						generic_param_update = true;
477					}
478					
479					_ if (position.after_substate(GenericParamPos::Name) || position == LexicalPosStage::WhereClause) => {
480						appendage = Some(TokenTree::Punct(p));
481					}
482					
483					_ => {}
484				}
485			}
486			
487			TokenTree::Literal(l) if (position.after_substate(GenericParamPos::Name) || position == LexicalPosStage::WhereClause) => {
488				appendage = Some(TokenTree::Literal(l));
489			}
490			
491			_ => {}
492		}
493		
494		'append_generics: {
495			if let Some(appendage) = appendage {
496				let append_to:&mut Option<TokenStream>;
497				if position.in_substate(GenericParamPos::Bounds) {
498					append_to = &mut current_generic_param.bounds;
499				} else if position.in_substate(GenericParamPos::DefaultAssignment) {
500					append_to = &mut current_generic_param.default_item;
501				} else if position == LexicalPosStage::WhereClause {
502					append_to = &mut where_clause;
503				} else {
504					break 'append_generics;
505				}
506				
507				if let &mut Some(ref mut append_to) = append_to {
508					append_to.extend([appendage].into_iter());
509				} else {
510					*append_to = Some(TokenStream::from_iter([appendage].into_iter()));
511				}
512			}
513		}
514		
515		'add_generic_param: {
516			if current_generic_param.name.is_none() { break 'add_generic_param; }
517			
518			if generic_param_update {
519				generic_params.push(current_generic_param);
520				current_generic_param = GenericParam::DEFAULT;
521			}
522		}
523		
524		prev_tok_as_punct = tok_as_punct;
525	}
526	
527	let mut r#impl = TokenStream::from_iter([
528		// #[inline(always)]
529		TokenTree::Punct(Punct::new('#', Spacing::Alone)),
530		TokenTree::Group(Group::new(
531			Delimiter::Bracket,
532			TokenStream::from_iter([
533				TokenTree::Ident(Ident::new("inline", Span::call_site())),
534				TokenTree::Group(Group::new(
535					Delimiter::Parenthesis,
536					TokenStream::from_iter([
537						TokenTree::Ident(Ident::new("always", Span::call_site()))
538					].into_iter())
539				))
540			].into_iter())
541		)),
542		
543		// /// ...
544		// pub const fn variant_count() -> ::core::primitive::usize
545		
546		TokenTree::Punct(Punct::new('#', Spacing::Alone)),
547		TokenTree::Group(Group::new(
548			Delimiter::Bracket,
549			TokenStream::from_iter([
550				TokenTree::Ident(Ident::new("doc", Span::call_site())),
551				TokenTree::Punct(Punct::new('=', Spacing::Alone)),
552				TokenTree::Literal(Literal::string("Returns the number of variants this enum has."))
553			].into_iter())
554		)),
555		TokenTree::Ident(Ident::new("pub", Span::call_site())),
556		TokenTree::Ident(Ident::new("const", Span::call_site())),
557		TokenTree::Ident(Ident::new("fn", Span::call_site())),
558		TokenTree::Ident(Ident::new("variant_count", Span::call_site())),
559		TokenTree::Group(Group::new(Delimiter::Parenthesis, TokenStream::new())),
560		TokenTree::Punct(Punct::new('-', Spacing::Joint)),
561		TokenTree::Punct(Punct::new('>', Spacing::Alone)),
562		TokenTree::Punct(Punct::new(':', Spacing::Joint)),
563		TokenTree::Punct(Punct::new(':', Spacing::Alone)),
564		TokenTree::Ident(Ident::new("core", Span::call_site())),
565		TokenTree::Punct(Punct::new(':', Spacing::Joint)),
566		TokenTree::Punct(Punct::new(':', Spacing::Alone)),
567		TokenTree::Ident(Ident::new("primitive", Span::call_site())),
568		TokenTree::Punct(Punct::new(':', Spacing::Joint)),
569		TokenTree::Punct(Punct::new(':', Spacing::Alone)),
570		TokenTree::Ident(Ident::new("usize", Span::call_site())),
571		
572		// { $variant_ct }
573		TokenTree::Group(Group::new(
574			Delimiter::Brace,
575			TokenStream::from_iter([
576				TokenTree::Literal(Literal::usize_unsuffixed(variant_ct))
577			].into_iter())
578		))
579	].into_iter());
580	
581	// pub const VARIANTS:[Self; $variant_ct] = [...$variant_names];
582	if all_unit {
583		let mut items = TokenStream::new();
584		
585		// [...$variant_names]
586		for variant in variant_names {
587			items.extend([
588				TokenTree::Ident(Ident::new("Self", Span::call_site())),
589				TokenTree::Punct(Punct::new(':', Spacing::Joint)),
590				TokenTree::Punct(Punct::new(':', Spacing::Alone)),
591				TokenTree::Ident(variant),
592				TokenTree::Punct(Punct::new(',', Spacing::Alone))
593			])
594		}
595		
596		r#impl.extend([
597			TokenTree::Ident(Ident::new("pub", Span::call_site())),
598			TokenTree::Ident(Ident::new("const", Span::call_site())),
599			TokenTree::Ident(Ident::new("VARIANTS", Span::call_site())),
600			TokenTree::Punct(Punct::new(':', Spacing::Alone)),
601			TokenTree::Group(Group::new(
602				Delimiter::Bracket,
603				TokenStream::from_iter([
604					TokenTree::Ident(Ident::new("Self", Span::call_site())),
605					TokenTree::Punct(Punct::new(';', Spacing::Alone)),
606					TokenTree::Literal(Literal::usize_unsuffixed(variant_ct))
607				].into_iter())
608			)),
609			TokenTree::Punct(Punct::new('=', Spacing::Alone)),
610			TokenTree::Group(Group::new(Delimiter::Bracket, items)),
611			TokenTree::Punct(Punct::new(';', Spacing::Alone))
612		]);
613	}
614	
615	let mut generics_names = TokenStream::new();
616	let mut generics_full = TokenStream::new();
617	
618	if generic_params.len() > 0 {
619		generics_names.extend([Punct::new('<', Spacing::Alone)]);
620		generics_full.extend([Punct::new('<', Spacing::Alone)]);
621		
622		for generic_item in generic_params {
623			if generic_item.param_type == Lifetime {
624				generics_names.extend([ unsafe { generic_item.param_type.prefix().unwrap_unchecked() } ]);
625			}
626			
627			generics_names.extend([
628				TokenTree::Ident(unsafe { generic_item.name.clone().unwrap_unchecked() }),
629				TokenTree::Punct(Punct::new(',', Spacing::Alone))
630			]);
631			
632			if let Some(prefix) = generic_item.param_type.prefix() {
633				generics_full.extend([prefix]);
634			}
635			
636			generics_full.extend([unsafe { generic_item.name.unwrap_unchecked() }]);
637			
638			if let Some(suffix) = generic_item.bounds {
639				generics_full.extend([
640					TokenTree::Punct(Punct::new(':', Spacing::Alone)),
641					TokenTree::Group(Group::new(Delimiter::None, suffix))
642				]);
643			}
644			
645			generics_full.extend([TokenTree::Punct(Punct::new(',', Spacing::Alone))]);
646		}
647		
648		generics_names.extend([Punct::new('>', Spacing::Alone)]);
649		generics_full.extend([Punct::new('>', Spacing::Alone)]);
650	}
651	
652	output.extend([
653		// impl<...$generic_parameters> $name<...$generic_parameter_names>
654		TokenTree::Ident(Ident::new("impl", Span::call_site())),
655		TokenTree::Group(Group::new(Delimiter::None, generics_full)),
656		TokenTree::Ident(Ident::new(&name, Span::call_site())),
657		TokenTree::Group(Group::new(Delimiter::None, generics_names)),
658		
659		// where $where_clause
660		TokenTree::Group(
661			Group::new(
662				Delimiter::None,
663				where_clause.map(
664					|ts| {
665						let mut w = TokenStream::from_iter([TokenTree::Ident(Ident::new("where", Span::call_site()))]);
666						w.extend(ts);
667						w
668					}
669				).unwrap_or_default()
670			)
671		),
672		
673		// { /* ... */ }
674		TokenTree::Group(Group::new(Delimiter::Brace, r#impl))
675	]);
676	output
677}