Skip to main content

miniextendr_macros/
struct_enum_dispatch.rs

1//! Dispatch `#[miniextendr]` on structs and enums to the appropriate derive helpers.
2//!
3//! This module handles `#[miniextendr]` when applied to structs or enums (not functions,
4//! impl blocks, or traits). It dispatches to the correct derive helper based on:
5//!
6//! - Field count (1-field → ALTREP by default, multi-field → ExternalPtr by default)
7//! - Explicit mode attributes (`list`, `dataframe`, `externalptr`, `match_arg`, `factor`)
8//! - Preference markers (`prefer = "..."`)
9//!
10//! # Disambiguation table
11//!
12//! | Syntax | Result |
13//! |---|---|
14//! | `#[miniextendr]` on 1-field struct | ALTREP (backwards compat) |
15//! | `#[miniextendr(externalptr)]` on 1-field struct | ExternalPtr |
16//! | `#[miniextendr(list)]` on 1-field struct | List conversion |
17//! | `#[miniextendr(class = "...", base = "...")]` on 1-field struct | ALTREP (explicit) |
18//! | `#[miniextendr]` on multi-field struct | ExternalPtr |
19//! | `#[miniextendr(list)]` on multi-field struct | IntoList + TryFromList + PreferList |
20//! | `#[miniextendr(dataframe)]` on multi-field struct | DataFrameRow + PreferDataFrame |
21//! | `#[miniextendr(prefer = "...")]` on struct | Prefer* marker |
22//! | `#[miniextendr]` on fieldless enum | RFactor |
23//! | `#[miniextendr(match_arg)]` on fieldless enum | MatchArg |
24
25/// Parsed attributes for `#[miniextendr]` on structs/enums.
26///
27/// These attributes control which derive path is taken when `#[miniextendr]`
28/// is applied to a struct or enum.
29struct StructEnumAttrs {
30    /// ALTREP class name override (forwarded to the ALTREP derive path).
31    class: Option<String>,
32    /// ALTREP base type override (forwarded to the ALTREP derive path).
33    base: Option<String>,
34    /// Derive `IntoList` + `TryFromList` + `PreferList` for struct-to-list round-tripping.
35    list: bool,
36    /// Derive `DataFrameRow` for struct-to-data-frame conversion.
37    dataframe: bool,
38    /// Force `ExternalPtr` derive even on single-field structs (which default to ALTREP).
39    externalptr: bool,
40    /// Derive `MatchArg` for enum (single-selection from R character scalar).
41    match_arg: bool,
42    /// Derive `RFactor` for enum (R factor representation).
43    factor: bool,
44    /// Preference marker string: `"externalptr"`, `"list"`, `"dataframe"`, or `"native"`.
45    /// Acts as a soft mode selector when no explicit mode attribute is set.
46    prefer: Option<String>,
47}
48
49/// Parses the attribute arguments of `#[miniextendr(...)]` when applied to a struct or enum.
50///
51/// Supports path-style flags (`list`, `dataframe`, `externalptr`, `match_arg`, `factor`)
52/// and key-value pairs (`class = "..."`, `base = "..."`, `prefer = "..."`).
53///
54/// Returns a [`StructEnumAttrs`] on success, or a compile error if an unknown attribute is found.
55fn parse_attrs(attr: proc_macro::TokenStream) -> syn::Result<StructEnumAttrs> {
56    use syn::parse::Parser;
57
58    let mut attrs = StructEnumAttrs {
59        class: None,
60        base: None,
61        list: false,
62        dataframe: false,
63        externalptr: false,
64        match_arg: false,
65        factor: false,
66        prefer: None,
67    };
68
69    // Parse as comma-separated meta items
70    let parser = syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated;
71    let metas = parser.parse(attr)?;
72
73    for meta in &metas {
74        match meta {
75            syn::Meta::Path(path) => {
76                let ident = path
77                    .get_ident()
78                    .ok_or_else(|| syn::Error::new_spanned(path, "expected identifier"))?;
79                match ident.to_string().as_str() {
80                    "list" => attrs.list = true,
81                    "dataframe" => attrs.dataframe = true,
82                    "externalptr" => attrs.externalptr = true,
83                    "match_arg" => attrs.match_arg = true,
84                    "factor" => attrs.factor = true,
85                    _ => {
86                        return Err(syn::Error::new_spanned(
87                            ident,
88                            format!(
89                                "unknown #[miniextendr] attribute `{}`; expected one of: \
90                                 list, dataframe, externalptr, match_arg, factor, prefer, class, base",
91                                ident
92                            ),
93                        ));
94                    }
95                }
96            }
97            syn::Meta::NameValue(nv) => {
98                let key = nv
99                    .path
100                    .get_ident()
101                    .map(|i| i.to_string())
102                    .unwrap_or_default();
103                if let syn::Expr::Lit(syn::ExprLit {
104                    lit: syn::Lit::Str(s),
105                    ..
106                }) = &nv.value
107                {
108                    match key.as_str() {
109                        "class" => attrs.class = Some(s.value()),
110                        "base" => attrs.base = Some(s.value()),
111                        "prefer" => attrs.prefer = Some(s.value()),
112                        _ => {
113                            return Err(syn::Error::new_spanned(
114                                &nv.path,
115                                format!(
116                                    "unknown #[miniextendr] attribute `{}`; expected one of: \
117                                     class, base, prefer",
118                                    key
119                                ),
120                            ));
121                        }
122                    }
123                }
124            }
125            syn::Meta::List(list) => {
126                return Err(syn::Error::new_spanned(
127                    list,
128                    "unexpected list-style attribute; use path (`list`) or key-value (`class = \"...\"`) syntax",
129                ));
130            }
131        }
132    }
133
134    Ok(attrs)
135}
136
137/// Returns the number of fields on a struct (named, unnamed, or unit).
138fn field_count(item: &syn::ItemStruct) -> usize {
139    match &item.fields {
140        syn::Fields::Named(f) => f.named.len(),
141        syn::Fields::Unnamed(f) => f.unnamed.len(),
142        syn::Fields::Unit => 0,
143    }
144}
145
146/// Returns `true` if every variant of the enum is a unit variant (no fields).
147fn is_fieldless_enum(item: &syn::ItemEnum) -> bool {
148    item.variants
149        .iter()
150        .all(|v| matches!(v.fields, syn::Fields::Unit))
151}
152
153/// Main dispatch entry point for `#[miniextendr]` on a struct or enum.
154///
155/// Attempts to parse the item as a struct first, then as an enum.
156/// Dispatches to the appropriate derive path based on the parsed attributes
157/// and item shape (field count, variant structure).
158///
159/// Returns the original item plus any generated trait implementations as a combined token stream.
160pub fn expand_struct_or_enum(
161    attr: proc_macro::TokenStream,
162    item: proc_macro::TokenStream,
163) -> proc_macro::TokenStream {
164    // Try parsing as struct first, then enum
165    if let Ok(item_struct) = syn::parse::<syn::ItemStruct>(item.clone()) {
166        return expand_struct(attr, item, &item_struct);
167    }
168
169    if let Ok(item_enum) = syn::parse::<syn::ItemEnum>(item.clone()) {
170        return expand_enum(attr, item, &item_enum);
171    }
172
173    // If neither, give a helpful error
174    syn::Error::new(
175        proc_macro2::Span::call_site(),
176        "#[miniextendr] on non-function items requires a struct or enum",
177    )
178    .into_compile_error()
179    .into()
180}
181
182/// Dispatches `#[miniextendr]` on a struct to the correct derive path.
183///
184/// Decision logic:
185/// - 1-field struct with no explicit mode: ALTREP (backwards compatibility)
186/// - Explicit `list` mode: `IntoList` + `TryFromList` + `PreferList`
187/// - Explicit `dataframe` mode: `IntoList` + `DataFrameRow` + companion `IntoR`
188/// - Default multi-field or explicit `externalptr`: `ExternalPtr`
189/// - `prefer = "native"`: `ExternalPtr` + `PreferRNative` marker
190fn expand_struct(
191    attr: proc_macro::TokenStream,
192    item: proc_macro::TokenStream,
193    item_struct: &syn::ItemStruct,
194) -> proc_macro::TokenStream {
195    let attrs = match parse_attrs(attr.clone()) {
196        Ok(a) => a,
197        Err(e) => return e.into_compile_error().into(),
198    };
199
200    let n_fields = field_count(item_struct);
201    let has_altrep_attrs = attrs.class.is_some() || attrs.base.is_some();
202    let has_mode_attr = attrs.list || attrs.dataframe || attrs.externalptr;
203
204    // Resolve prefer into a mode if no explicit mode attr is set
205    let effective_list = attrs.list || (!has_mode_attr && attrs.prefer.as_deref() == Some("list"));
206    let effective_dataframe =
207        attrs.dataframe || (!has_mode_attr && attrs.prefer.as_deref() == Some("dataframe"));
208    let effective_externalptr =
209        attrs.externalptr || (!has_mode_attr && attrs.prefer.as_deref() == Some("externalptr"));
210    let effective_mode = effective_list || effective_dataframe || effective_externalptr;
211
212    // 1-field struct with explicit ALTREP attrs: error with migration guidance.
213    // ALTREP via #[miniextendr] is removed — use #[derive(Altrep)] instead.
214    if n_fields == 1 && has_altrep_attrs && !effective_mode {
215        return syn::Error::new(
216            item_struct.ident.span(),
217            "#[miniextendr] no longer supports ALTREP (class/base attributes). \
218             Use #[derive(miniextendr_api::Altrep)] with #[altrep(class = \"...\")] instead.",
219        )
220        .into_compile_error()
221        .into();
222    }
223
224    // Reject ALTREP attrs on non-ALTREP paths
225    if has_altrep_attrs && effective_mode {
226        return syn::Error::new(
227            item_struct.ident.span(),
228            "cannot combine ALTREP attributes (class, base) with mode attributes (list, dataframe, externalptr)",
229        )
230        .into_compile_error()
231        .into();
232    }
233
234    // Check for conflicting mode attrs
235    let mode_count = [effective_list, effective_dataframe, effective_externalptr]
236        .iter()
237        .filter(|&&b| b)
238        .count();
239    if mode_count > 1 {
240        return syn::Error::new(
241            item_struct.ident.span(),
242            "only one of `list`, `dataframe`, `externalptr` can be specified",
243        )
244        .into_compile_error()
245        .into();
246    }
247
248    // Validate prefer value
249    if let Some(ref prefer) = attrs.prefer
250        && !matches!(
251            prefer.as_str(),
252            "externalptr" | "list" | "dataframe" | "native"
253        )
254    {
255        return syn::Error::new(
256            item_struct.ident.span(),
257            format!(
258                "unknown prefer value `{}`; expected one of: externalptr, list, dataframe, native",
259                prefer
260            ),
261        )
262        .into_compile_error()
263        .into();
264    }
265
266    // Convert to DeriveInput for the derive helpers
267    let derive_input: syn::DeriveInput = match syn::parse(item.clone()) {
268        Ok(d) => d,
269        Err(e) => return e.into_compile_error().into(),
270    };
271    // Strip #[miniextendr(...)] from the DeriveInput attrs — derive helpers don't expect them
272    let derive_input = strip_miniextendr_attrs(derive_input);
273
274    let item_ts: proc_macro2::TokenStream = item.into();
275
276    if effective_list {
277        // List mode: IntoList + TryFromList + PreferList
278        let result = (|| -> syn::Result<proc_macro2::TokenStream> {
279            let into_list = crate::list_derive::derive_into_list(derive_input.clone())?;
280            let try_from_list = crate::list_derive::derive_try_from_list(derive_input.clone())?;
281            let prefer_list = crate::list_derive::derive_prefer_list(derive_input)?;
282            Ok(quote::quote! {
283                #item_ts
284                #into_list
285                #try_from_list
286                #prefer_list
287            })
288        })();
289        return result.unwrap_or_else(|e| e.into_compile_error()).into();
290    }
291
292    if effective_dataframe {
293        // DataFrame mode: IntoList + DataFrameRow (which now includes companion IntoR)
294        // IntoList is required by DataFrameRow's trait assertion.
295        let result = (|| -> syn::Result<proc_macro2::TokenStream> {
296            let into_list = crate::list_derive::derive_into_list(derive_input.clone())?;
297            let dataframe_row = crate::dataframe_derive::derive_dataframe_row(derive_input)?;
298            Ok(quote::quote! {
299                #item_ts
300                #into_list
301                #dataframe_row
302            })
303        })();
304        return result.unwrap_or_else(|e| e.into_compile_error()).into();
305    }
306
307    // Default for multi-field or explicit externalptr: ExternalPtr
308    // Also handles prefer = "native" (ExternalPtr + native preference marker)
309    let result = (|| -> syn::Result<proc_macro2::TokenStream> {
310        let external_ptr = crate::externalptr_derive::derive_external_ptr(derive_input.clone())?;
311
312        // Apply native preference marker if specified
313        let prefer = if attrs.prefer.as_deref() == Some("native") {
314            crate::list_derive::derive_prefer_rnative(derive_input)?
315        } else {
316            proc_macro2::TokenStream::new()
317        };
318
319        Ok(quote::quote! {
320            #item_ts
321            #external_ptr
322            #prefer
323        })
324    })();
325    result.unwrap_or_else(|e| e.into_compile_error()).into()
326}
327
328/// Dispatches `#[miniextendr]` on a fieldless enum to the correct derive path.
329///
330/// Only C-style (fieldless) enums are supported. Dispatches to:
331/// - `match_arg` mode: `MatchArg` derive (single-selection from R character scalar)
332/// - `factor` mode or default: `RFactor` derive (R factor representation)
333fn expand_enum(
334    attr: proc_macro::TokenStream,
335    item: proc_macro::TokenStream,
336    item_enum: &syn::ItemEnum,
337) -> proc_macro::TokenStream {
338    let attrs = match parse_attrs(attr) {
339        Ok(a) => a,
340        Err(e) => return e.into_compile_error().into(),
341    };
342
343    if !is_fieldless_enum(item_enum) {
344        return syn::Error::new(
345            item_enum.ident.span(),
346            "#[miniextendr] on enums requires all variants to be fieldless (C-style)",
347        )
348        .into_compile_error()
349        .into();
350    }
351
352    let derive_input: syn::DeriveInput = match syn::parse(item.clone()) {
353        Ok(d) => d,
354        Err(e) => return e.into_compile_error().into(),
355    };
356    let derive_input = strip_miniextendr_attrs(derive_input);
357
358    let item_ts: proc_macro2::TokenStream = item.into();
359
360    if attrs.match_arg {
361        // MatchArg mode
362        let result = crate::match_arg_derive::derive_match_arg(derive_input);
363        return match result {
364            Ok(ts) => quote::quote! { #item_ts #ts }.into(),
365            Err(e) => e.into_compile_error().into(),
366        };
367    }
368
369    if attrs.factor {
370        // Explicit factor mode
371        let result = crate::factor_derive::derive_r_factor(derive_input);
372        return match result {
373            Ok(ts) => quote::quote! { #item_ts #ts }.into(),
374            Err(e) => e.into_compile_error().into(),
375        };
376    }
377
378    // Default: RFactor
379    let result = crate::factor_derive::derive_r_factor(derive_input);
380    match result {
381        Ok(ts) => quote::quote! { #item_ts #ts }.into(),
382        Err(e) => e.into_compile_error().into(),
383    }
384}
385
386/// Strip `#[miniextendr(...)]` attributes from a DeriveInput.
387///
388/// Derive helpers don't expect `#[miniextendr]` attributes and may fail or
389/// misinterpret them. We strip them before forwarding.
390fn strip_miniextendr_attrs(mut input: syn::DeriveInput) -> syn::DeriveInput {
391    input
392        .attrs
393        .retain(|attr| !attr.path().is_ident("miniextendr"));
394    input
395}