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 ALTREP attrs (`class`/`base`): compile error with
186///   migration guidance (ALTREP via `#[miniextendr]` is removed — use the
187///   `#[derive(Altrep*)]` family)
188/// - Explicit `list` mode: `IntoList` + `TryFromList` + `PreferList`
189/// - Explicit `dataframe` mode: `IntoList` + `DataFrameRow` + companion `IntoR`
190/// - Default multi-field or explicit `externalptr`: `ExternalPtr`
191/// - `prefer = "native"`: `ExternalPtr` (without the `IntoExternalPtr` marker,
192///   #1283) + `PreferRNativeType` marker
193fn expand_struct(
194    attr: proc_macro::TokenStream,
195    item: proc_macro::TokenStream,
196    item_struct: &syn::ItemStruct,
197) -> proc_macro::TokenStream {
198    let attrs = match parse_attrs(attr.clone()) {
199        Ok(a) => a,
200        Err(e) => return e.into_compile_error().into(),
201    };
202
203    let n_fields = field_count(item_struct);
204    let has_altrep_attrs = attrs.class.is_some() || attrs.base.is_some();
205    let has_mode_attr = attrs.list || attrs.dataframe || attrs.externalptr;
206
207    // Resolve prefer into a mode if no explicit mode attr is set
208    let effective_list = attrs.list || (!has_mode_attr && attrs.prefer.as_deref() == Some("list"));
209    let effective_dataframe =
210        attrs.dataframe || (!has_mode_attr && attrs.prefer.as_deref() == Some("dataframe"));
211    let effective_externalptr =
212        attrs.externalptr || (!has_mode_attr && attrs.prefer.as_deref() == Some("externalptr"));
213    let effective_mode = effective_list || effective_dataframe || effective_externalptr;
214
215    // 1-field struct with explicit ALTREP attrs: error with migration guidance.
216    // ALTREP via #[miniextendr] is removed — use #[derive(Altrep)] instead.
217    if n_fields == 1 && has_altrep_attrs && !effective_mode {
218        return syn::Error::new(
219            item_struct.ident.span(),
220            "#[miniextendr] no longer supports ALTREP (class/base attributes). \
221             Use #[derive(miniextendr_api::Altrep)] with #[altrep(class = \"...\")] instead.",
222        )
223        .into_compile_error()
224        .into();
225    }
226
227    // Reject ALTREP attrs on non-ALTREP paths
228    if has_altrep_attrs && effective_mode {
229        return syn::Error::new(
230            item_struct.ident.span(),
231            "cannot combine ALTREP attributes (class, base) with mode attributes (list, dataframe, externalptr)",
232        )
233        .into_compile_error()
234        .into();
235    }
236
237    // Check for conflicting mode attrs
238    let mode_count = [effective_list, effective_dataframe, effective_externalptr]
239        .iter()
240        .filter(|&&b| b)
241        .count();
242    if mode_count > 1 {
243        return syn::Error::new(
244            item_struct.ident.span(),
245            "only one of `list`, `dataframe`, `externalptr` can be specified",
246        )
247        .into_compile_error()
248        .into();
249    }
250
251    // Validate prefer value
252    if let Some(ref prefer) = attrs.prefer
253        && !matches!(
254            prefer.as_str(),
255            "externalptr" | "list" | "dataframe" | "native"
256        )
257    {
258        return syn::Error::new(
259            item_struct.ident.span(),
260            format!(
261                "unknown prefer value `{}`; expected one of: externalptr, list, dataframe, native",
262                prefer
263            ),
264        )
265        .into_compile_error()
266        .into();
267    }
268
269    // Convert to DeriveInput for the derive helpers
270    let derive_input: syn::DeriveInput = match syn::parse(item.clone()) {
271        Ok(d) => d,
272        Err(e) => return e.into_compile_error().into(),
273    };
274    // Strip #[miniextendr(...)] from the DeriveInput attrs — derive helpers don't expect them
275    let derive_input = strip_miniextendr_attrs(derive_input);
276
277    let item_ts: proc_macro2::TokenStream = item.into();
278
279    if effective_list {
280        // List mode: IntoList + TryFromList + PreferList
281        let result = (|| -> syn::Result<proc_macro2::TokenStream> {
282            let into_list = crate::list_derive::derive_into_list(derive_input.clone())?;
283            let try_from_list = crate::list_derive::derive_try_from_list(derive_input.clone())?;
284            let prefer_list = crate::list_derive::derive_prefer_list(derive_input)?;
285            Ok(quote::quote! {
286                #item_ts
287                #into_list
288                #try_from_list
289                #prefer_list
290            })
291        })();
292        return result.unwrap_or_else(|e| e.into_compile_error()).into();
293    }
294
295    if effective_dataframe {
296        // DataFrame mode: IntoList + DataFrameRow (which now includes companion IntoR)
297        // IntoList is required by DataFrameRow's trait assertion.
298        let result = (|| -> syn::Result<proc_macro2::TokenStream> {
299            let into_list = crate::list_derive::derive_into_list(derive_input.clone())?;
300            let dataframe_row = crate::dataframe_derive::derive_dataframe_row(derive_input)?;
301            Ok(quote::quote! {
302                #item_ts
303                #into_list
304                #dataframe_row
305            })
306        })();
307        return result.unwrap_or_else(|e| e.into_compile_error()).into();
308    }
309
310    // Default for multi-field or explicit externalptr: ExternalPtr
311    // Also handles prefer = "native" (ExternalPtr + native preference marker)
312    let result = (|| -> syn::Result<proc_macro2::TokenStream> {
313        // Suppress the `IntoExternalPtr` marker (and its blanket `IntoR`) for
314        // `prefer = "native"` so the concrete `AsRNative` `IntoR` from
315        // `derive_prefer_rnative` below is the type's sole `IntoR` (E0119, #1283).
316        let external_ptr = crate::externalptr_derive::derive_external_ptr(
317            derive_input.clone(),
318            attrs.prefer.as_deref() != Some("native"),
319        )?;
320
321        // Apply native preference marker if specified
322        let prefer = if attrs.prefer.as_deref() == Some("native") {
323            crate::list_derive::derive_prefer_rnative(derive_input)?
324        } else {
325            proc_macro2::TokenStream::new()
326        };
327
328        Ok(quote::quote! {
329            #item_ts
330            #external_ptr
331            #prefer
332        })
333    })();
334    result.unwrap_or_else(|e| e.into_compile_error()).into()
335}
336
337/// Dispatches `#[miniextendr]` on a fieldless enum to the correct derive path.
338///
339/// Only C-style (fieldless) enums are supported. Dispatches to:
340/// - `match_arg` mode: `MatchArg` derive (single-selection from R character scalar)
341/// - `factor` mode or default: `RFactor` derive (R factor representation)
342fn expand_enum(
343    attr: proc_macro::TokenStream,
344    item: proc_macro::TokenStream,
345    item_enum: &syn::ItemEnum,
346) -> proc_macro::TokenStream {
347    let attrs = match parse_attrs(attr) {
348        Ok(a) => a,
349        Err(e) => return e.into_compile_error().into(),
350    };
351
352    if !is_fieldless_enum(item_enum) {
353        return syn::Error::new(
354            item_enum.ident.span(),
355            "#[miniextendr] on enums requires all variants to be fieldless (C-style)",
356        )
357        .into_compile_error()
358        .into();
359    }
360
361    let derive_input: syn::DeriveInput = match syn::parse(item.clone()) {
362        Ok(d) => d,
363        Err(e) => return e.into_compile_error().into(),
364    };
365    let derive_input = strip_miniextendr_attrs(derive_input);
366
367    let item_ts: proc_macro2::TokenStream = item.into();
368
369    if attrs.match_arg {
370        // MatchArg mode
371        let result = crate::match_arg_derive::derive_match_arg(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    if attrs.factor {
379        // Explicit factor mode
380        let result = crate::factor_derive::derive_r_factor(derive_input);
381        return match result {
382            Ok(ts) => quote::quote! { #item_ts #ts }.into(),
383            Err(e) => e.into_compile_error().into(),
384        };
385    }
386
387    // Default: RFactor
388    let result = crate::factor_derive::derive_r_factor(derive_input);
389    match result {
390        Ok(ts) => quote::quote! { #item_ts #ts }.into(),
391        Err(e) => e.into_compile_error().into(),
392    }
393}
394
395/// Strip `#[miniextendr(...)]` attributes from a DeriveInput.
396///
397/// Derive helpers don't expect `#[miniextendr]` attributes and may fail or
398/// misinterpret them. We strip them before forwarding.
399fn strip_miniextendr_attrs(mut input: syn::DeriveInput) -> syn::DeriveInput {
400    input
401        .attrs
402        .retain(|attr| !attr.path().is_ident("miniextendr"));
403    input
404}