1#![forbid(unsafe_code)]
16
17use proc_macro::{Delimiter, Group, Ident, Punct, Spacing, TokenStream, TokenTree};
18
19#[proc_macro_attribute]
20pub fn odin_union(arguments: TokenStream, item: TokenStream) -> TokenStream {
21 if !arguments.is_empty() {
22 return compile_error("`odin_union` does not accept arguments");
23 }
24
25 expand(item).unwrap_or_else(|message| compile_error(&message))
26}
27
28#[derive(Debug)]
29struct Variant {
30 name: Ident,
31 conditional_attributes: TokenStream,
32}
33
34fn expand(item: TokenStream) -> Result<TokenStream, String> {
35 let mut item_tokens: Vec<TokenTree> = item.into_iter().collect();
36
37 let enum_index = item_tokens
38 .iter()
39 .position(|token| is_ident(token, "enum"))
40 .ok_or_else(|| "`odin_union` can only be applied to an enum".to_owned())?;
41
42 let name_index = enum_index + 1;
43 let enum_name = match item_tokens.get(name_index) {
44 Some(TokenTree::Ident(name)) => name.clone(),
45 _ => return Err("expected an enum name after `enum`".to_owned()),
46 };
47
48 let body_index = item_tokens
49 .iter()
50 .enumerate()
51 .skip(name_index + 1)
52 .find_map(|(index, token)| match token {
53 TokenTree::Group(group) if group.delimiter() == Delimiter::Brace => Some(index),
54 _ => None,
55 })
56 .ok_or_else(|| "expected an enum body".to_owned())?;
57
58 if body_index != name_index + 1 {
59 return Err(
60 "generic enums are not supported because payload type arguments cannot be inferred"
61 .to_owned(),
62 );
63 }
64
65 let original_body = match &item_tokens[body_index] {
66 TokenTree::Group(group) => group,
67 _ => unreachable!("the enum body index always points to a group"),
68 };
69
70 let (expanded_body, variants) = expand_variants(original_body.stream())?;
71 let mut replacement_body = Group::new(Delimiter::Brace, expanded_body);
72 replacement_body.set_span(original_body.span());
73 item_tokens[body_index] = TokenTree::Group(replacement_body);
74
75 let mut output: TokenStream = item_tokens.into_iter().collect();
76 for variant in variants {
77 output.extend(from_implementation(&enum_name, &variant)?);
78 }
79
80 Ok(output)
81}
82
83fn expand_variants(body: TokenStream) -> Result<(TokenStream, Vec<Variant>), String> {
84 let variant_groups = split_variants(body)?;
85 let mut expanded_body = TokenStream::new();
86 let mut variants = Vec::with_capacity(variant_groups.len());
87
88 for variant_tokens in variant_groups {
89 let (attributes, conditional_attributes, name) = parse_variant(variant_tokens)?;
90
91 expanded_body.extend(attributes);
92 expanded_body.extend([TokenTree::Ident(name.clone())]);
93
94 let payload: TokenStream = [TokenTree::Ident(name.clone())].into_iter().collect();
95 let mut payload_group = Group::new(Delimiter::Parenthesis, payload);
96 payload_group.set_span(name.span());
97 expanded_body.extend([TokenTree::Group(payload_group)]);
98
99 let mut comma = Punct::new(',', Spacing::Alone);
100 comma.set_span(name.span());
101 expanded_body.extend([TokenTree::Punct(comma)]);
102
103 variants.push(Variant {
104 name,
105 conditional_attributes,
106 });
107 }
108
109 Ok((expanded_body, variants))
110}
111
112fn split_variants(body: TokenStream) -> Result<Vec<Vec<TokenTree>>, String> {
113 let mut variants = Vec::new();
114 let mut current = Vec::new();
115
116 for token in body {
117 if matches!(&token, TokenTree::Punct(punctuation) if punctuation.as_char() == ',') {
118 if current.is_empty() {
119 return Err("expected a variant before `,`".to_owned());
120 }
121 variants.push(current);
122 current = Vec::new();
123 } else {
124 current.push(token);
125 }
126 }
127
128 if !current.is_empty() {
129 variants.push(current);
130 }
131
132 Ok(variants)
133}
134
135fn parse_variant(tokens: Vec<TokenTree>) -> Result<(TokenStream, TokenStream, Ident), String> {
136 let mut index = 0;
137 let mut attributes = TokenStream::new();
138 let mut conditional_attributes = TokenStream::new();
139
140 while matches!(tokens.get(index), Some(TokenTree::Punct(punctuation)) if punctuation.as_char() == '#')
141 {
142 let hash = tokens[index].clone();
143 let attribute = match tokens.get(index + 1) {
144 Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Bracket => {
145 group.clone()
146 }
147 _ => return Err("expected an attribute after `#`".to_owned()),
148 };
149
150 attributes.extend([hash.clone(), TokenTree::Group(attribute.clone())]);
151
152 if attribute_is_cfg(&attribute) {
153 conditional_attributes.extend([hash, TokenTree::Group(attribute)]);
154 }
155
156 index += 2;
157 }
158
159 let name = match tokens.get(index) {
160 Some(TokenTree::Ident(name)) => name.clone(),
161 _ => return Err("expected a bare enum variant".to_owned()),
162 };
163
164 if index + 1 != tokens.len() {
165 return Err(format!(
166 "variant `{name}` must be bare; write `{name}`, not `{name}(...)`, `{name} {{ ... }}`, or `{name} = ...`"
167 ));
168 }
169
170 Ok((attributes, conditional_attributes, name))
171}
172
173fn attribute_is_cfg(attribute: &Group) -> bool {
174 matches!(
175 attribute.stream().into_iter().next(),
176 Some(TokenTree::Ident(name)) if name.to_string() == "cfg"
177 )
178}
179
180fn from_implementation(enum_name: &Ident, variant: &Variant) -> Result<TokenStream, String> {
181 let name = &variant.name;
182 let implementation = format!(
183 "impl ::core::convert::From<{name}> for {enum_name} {{
184 fn from(value: {name}) -> Self {{
185 Self::{name}(value)
186 }}
187 }}"
188 );
189
190 let implementation: TokenStream = implementation
191 .parse()
192 .map_err(|_| format!("failed to generate `From<{name}> for {enum_name}`"))?;
193
194 let mut output = variant.conditional_attributes.clone();
195 output.extend(implementation);
196 Ok(output)
197}
198
199fn is_ident(token: &TokenTree, expected: &str) -> bool {
200 matches!(token, TokenTree::Ident(identifier) if identifier.to_string() == expected)
201}
202
203fn compile_error(message: &str) -> TokenStream {
204 format!("compile_error!({message:?});")
205 .parse()
206 .expect("a quoted string always produces a valid `compile_error!` invocation")
207}