Skip to main content

odin_union/
lib.rs

1// Copyright (C) 2026  Sisyphus1813
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11// GNU General Public License for more details.
12// You should have received a copy of the GNU General Public License
13// along with this program.  If not, see <https://www.gnu.org/licenses/>.
14
15#![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}