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//! - Item shape (structs → ExternalPtr, fieldless enums → RFactor 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]` / `#[miniextendr(externalptr)]` on struct | ExternalPtr |
15//! | `#[miniextendr(list)]` on struct | IntoList + TryFromList + PreferList |
16//! | `#[miniextendr(dataframe)]` on struct | DataFrameRow (companion has IntoR) |
17//! | `#[miniextendr(prefer = "list" / "dataframe" / "externalptr")]` on struct | Same as the explicit mode |
18//! | `#[miniextendr(prefer = "native")]` on struct | ExternalPtr without its IntoR + PreferRNativeType |
19//! | `#[miniextendr(class = "...", base = "...")]` on struct | Error: use an ALTREP derive |
20//! | `#[miniextendr]` on fieldless enum | RFactor |
21//! | `#[miniextendr(match_arg)]` on fieldless enum | MatchArg |
22
23/// Parsed attributes for `#[miniextendr]` on structs/enums.
24///
25/// These attributes control which derive path is taken when `#[miniextendr]`
26/// is applied to a struct or enum.
27struct StructEnumAttrs {
28    /// Removed ALTREP class option; parsed only to reject it with migration guidance.
29    class: Option<String>,
30    /// Removed ALTREP base option; parsed only to reject it with migration guidance.
31    base: Option<String>,
32    /// Derive `IntoList` + `TryFromList` + `PreferList` for struct-to-list round-tripping.
33    list: bool,
34    /// Derive `DataFrameRow` for struct-to-data-frame conversion.
35    dataframe: bool,
36    /// Explicitly select the `ExternalPtr` derive.
37    externalptr: bool,
38    /// Derive `MatchArg` for enum (single-selection from R character scalar).
39    match_arg: bool,
40    /// Derive `RFactor` for enum (R factor representation).
41    factor: bool,
42    /// Preference marker string: `"externalptr"`, `"list"`, `"dataframe"`, or `"native"`.
43    /// Acts as a soft mode selector when no explicit mode attribute is set.
44    prefer: Option<String>,
45}
46
47/// Parses the attribute arguments of `#[miniextendr(...)]` when applied to a struct or enum.
48///
49/// Supports path-style flags (`list`, `dataframe`, `externalptr`, `match_arg`, `factor`)
50/// and key-value pairs (`class = "..."`, `base = "..."`, `prefer = "..."`).
51///
52/// Returns a [`StructEnumAttrs`] on success, or a compile error if an unknown attribute is found.
53fn parse_attrs(attr: proc_macro::TokenStream) -> syn::Result<StructEnumAttrs> {
54    use syn::parse::Parser;
55
56    let mut attrs = StructEnumAttrs {
57        class: None,
58        base: None,
59        list: false,
60        dataframe: false,
61        externalptr: false,
62        match_arg: false,
63        factor: false,
64        prefer: None,
65    };
66
67    // Parse as comma-separated meta items
68    let parser = syn::punctuated::Punctuated::<syn::Meta, ::syn::token::Commasyn::Token![,]>::parse_terminated;
69    let metas = parser.parse(attr)?;
70
71    for meta in &metas {
72        match meta {
73            syn::Meta::Path(path) => {
74                let ident = path
75                    .get_ident()
76                    .ok_or_else(|| syn::Error::new_spanned(path, "expected identifier"))?;
77                match ident.to_string().as_str() {
78                    "list" => attrs.list = true,
79                    "dataframe" => attrs.dataframe = true,
80                    "externalptr" => attrs.externalptr = true,
81                    "match_arg" => attrs.match_arg = true,
82                    "factor" => attrs.factor = true,
83                    _ => {
84                        return Err(syn::Error::new_spanned(
85                            ident,
86                            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unknown #[miniextendr] attribute `{0}`; expected one of: list, dataframe, externalptr, match_arg, factor, prefer, class, base",
                ident))
    })format!(
87                                "unknown #[miniextendr] attribute `{}`; expected one of: \
88                                 list, dataframe, externalptr, match_arg, factor, prefer, class, base",
89                                ident
90                            ),
91                        ));
92                    }
93                }
94            }
95            syn::Meta::NameValue(nv) => {
96                let key = nv
97                    .path
98                    .get_ident()
99                    .map(|i| i.to_string())
100                    .unwrap_or_default();
101                if let syn::Expr::Lit(syn::ExprLit {
102                    lit: syn::Lit::Str(s),
103                    ..
104                }) = &nv.value
105                {
106                    match key.as_str() {
107                        "class" => attrs.class = Some(s.value()),
108                        "base" => attrs.base = Some(s.value()),
109                        "prefer" => attrs.prefer = Some(s.value()),
110                        _ => {
111                            return Err(syn::Error::new_spanned(
112                                &nv.path,
113                                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unknown #[miniextendr] attribute `{0}`; expected one of: class, base, prefer",
                key))
    })format!(
114                                    "unknown #[miniextendr] attribute `{}`; expected one of: \
115                                     class, base, prefer",
116                                    key
117                                ),
118                            ));
119                        }
120                    }
121                }
122            }
123            syn::Meta::List(list) => {
124                return Err(syn::Error::new_spanned(
125                    list,
126                    "unexpected list-style attribute; use path (`list`) or key-value (`class = \"...\"`) syntax",
127                ));
128            }
129        }
130    }
131
132    Ok(attrs)
133}
134
135/// Returns `true` if every variant of the enum is a unit variant (no fields).
136fn is_fieldless_enum(item: &syn::ItemEnum) -> bool {
137    item.variants
138        .iter()
139        .all(|v| #[allow(non_exhaustive_omitted_patterns)] match v.fields {
    syn::Fields::Unit => true,
    _ => false,
}matches!(v.fields, syn::Fields::Unit))
140}
141
142/// Main dispatch entry point for `#[miniextendr]` on a struct or enum.
143///
144/// Attempts to parse the item as a struct first, then as an enum.
145/// Dispatches to the appropriate derive path based on the parsed attributes
146/// and item shape (field count, variant structure).
147///
148/// Returns the original item plus any generated trait implementations as a combined token stream.
149pub fn expand_struct_or_enum(
150    attr: proc_macro::TokenStream,
151    item: proc_macro::TokenStream,
152) -> proc_macro::TokenStream {
153    // Try parsing as struct first, then enum
154    if let Ok(item_struct) = syn::parse::<syn::ItemStruct>(item.clone()) {
155        return expand_struct(attr, item, &item_struct);
156    }
157
158    if let Ok(item_enum) = syn::parse::<syn::ItemEnum>(item.clone()) {
159        return expand_enum(attr, item, &item_enum);
160    }
161
162    // A function (or a method in an impl or trait) whose Rust `...` is not the
163    // last parameter doesn't parse as one, so it lands here: name the cause (#1737).
164    if let Some(found) = crate::misplaced_dots::find(item.into()) {
165        return syn::Error::new_spanned(
166            found.dots,
167            crate::misplaced_dots::message(found.name.as_deref()),
168        )
169        .into_compile_error()
170        .into();
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/// - ALTREP attrs (`class`/`base`): compile error with migration guidance
186///   (ALTREP via `#[miniextendr]` is removed — use the `#[derive(Altrep*)]`
187///   family)
188/// - `list` mode: `#[derive(IntoList, TryFromList, PreferList)]`
189/// - `dataframe` mode: `#[derive(DataFrameRow)]` (the companion gets `IntoR`)
190/// - Default 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 has_altrep_attrs = attrs.class.is_some() || attrs.base.is_some();
204    let has_mode_attr = attrs.list || attrs.dataframe || attrs.externalptr;
205
206    // Resolve prefer into a mode if no explicit mode attr is set
207    let effective_list = attrs.list || (!has_mode_attr && attrs.prefer.as_deref() == Some("list"));
208    let effective_dataframe =
209        attrs.dataframe || (!has_mode_attr && attrs.prefer.as_deref() == Some("dataframe"));
210    let effective_externalptr =
211        attrs.externalptr || (!has_mode_attr && attrs.prefer.as_deref() == Some("externalptr"));
212    let effective_mode = effective_list || effective_dataframe || effective_externalptr;
213
214    // ALTREP attrs without a mode: error with migration guidance.
215    // ALTREP via #[miniextendr] is removed — use #[derive(Altrep)] instead.
216    if has_altrep_attrs && !effective_mode {
217        return syn::Error::new(
218            item_struct.ident.span(),
219            "#[miniextendr] no longer supports ALTREP (class/base attributes). \
220             Use #[derive(miniextendr_api::Altrep)] with #[altrep(class = \"...\")] instead.",
221        )
222        .into_compile_error()
223        .into();
224    }
225
226    // Reject ALTREP attrs on non-ALTREP paths
227    if has_altrep_attrs && effective_mode {
228        return syn::Error::new(
229            item_struct.ident.span(),
230            "cannot combine ALTREP attributes (class, base) with mode attributes (list, dataframe, externalptr)",
231        )
232        .into_compile_error()
233        .into();
234    }
235
236    // Check for conflicting mode attrs
237    let mode_count = [effective_list, effective_dataframe, effective_externalptr]
238        .iter()
239        .filter(|&&b| b)
240        .count();
241    if mode_count > 1 {
242        return syn::Error::new(
243            item_struct.ident.span(),
244            "only one of `list`, `dataframe`, `externalptr` can be specified",
245        )
246        .into_compile_error()
247        .into();
248    }
249
250    // Validate prefer value
251    if let Some(ref prefer) = attrs.prefer
252        && !#[allow(non_exhaustive_omitted_patterns)] match prefer.as_str() {
    "externalptr" | "list" | "dataframe" | "native" => true,
    _ => false,
}matches!(
253            prefer.as_str(),
254            "externalptr" | "list" | "dataframe" | "native"
255        )
256    {
257        return syn::Error::new(
258            item_struct.ident.span(),
259            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unknown prefer value `{0}`; expected one of: externalptr, list, dataframe, native",
                prefer))
    })format!(
260                "unknown prefer value `{}`; expected one of: externalptr, list, dataframe, native",
261                prefer
262            ),
263        )
264        .into_compile_error()
265        .into();
266    }
267
268    // Emit actual derives so rustc registers their helper attributes (#2024).
269    // Calling their implementations directly would leave unclaimed helpers on
270    // the struct; stripping them would hide options from other derives.
271    if effective_list {
272        return {
    let mut _s = ::quote::__private::TokenStream::new();
    ::quote::__private::push_pound(&mut _s);
    ::quote::__private::push_group(&mut _s,
        ::quote::__private::Delimiter::Bracket,
        {
            let mut _s = ::quote::__private::TokenStream::new();
            ::quote::__private::push_ident(&mut _s, "derive");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Parenthesis,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "IntoList");
                    ::quote::__private::push_comma(&mut _s);
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "TryFromList");
                    ::quote::__private::push_comma(&mut _s);
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "PreferList");
                    ::quote::__private::push_comma(&mut _s);
                    _s
                });
            _s
        });
    ::quote::ToTokens::to_tokens(&item_struct, &mut _s);
    _s
}quote::quote! {
273            #[derive(
274                ::miniextendr_api::IntoList,
275                ::miniextendr_api::TryFromList,
276                ::miniextendr_api::PreferList,
277            )]
278            #item_struct
279        }
280        .into();
281    }
282
283    if effective_dataframe {
284        // Row columns do not need an IntoList implementation on the row.
285        return {
    let mut _s = ::quote::__private::TokenStream::new();
    ::quote::__private::push_pound(&mut _s);
    ::quote::__private::push_group(&mut _s,
        ::quote::__private::Delimiter::Bracket,
        {
            let mut _s = ::quote::__private::TokenStream::new();
            ::quote::__private::push_ident(&mut _s, "derive");
            ::quote::__private::push_group(&mut _s,
                ::quote::__private::Delimiter::Parenthesis,
                {
                    let mut _s = ::quote::__private::TokenStream::new();
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "miniextendr_api");
                    ::quote::__private::push_colon2(&mut _s);
                    ::quote::__private::push_ident(&mut _s, "DataFrameRow");
                    _s
                });
            _s
        });
    ::quote::ToTokens::to_tokens(&item_struct, &mut _s);
    _s
}quote::quote! {
286            #[derive(::miniextendr_api::DataFrameRow)]
287            #item_struct
288        }
289        .into();
290    }
291
292    // The ExternalPtr helper needs the native preference flag below.
293    let derive_input: syn::DeriveInput = match syn::parse(item.clone()) {
294        Ok(d) => d,
295        Err(e) => return e.into_compile_error().into(),
296    };
297    let derive_input = strip_miniextendr_attrs(derive_input);
298    let item_ts: proc_macro2::TokenStream = item.into();
299
300    // Default for multi-field or explicit externalptr: ExternalPtr
301    // Also handles prefer = "native" (ExternalPtr + native preference marker)
302    let result = (|| -> syn::Result<proc_macro2::TokenStream> {
303        // Suppress the `IntoExternalPtr` marker (and its blanket `IntoR`) for
304        // `prefer = "native"` so the concrete `AsRNative` `IntoR` from
305        // `derive_prefer_rnative` below is the type's sole `IntoR` (E0119, #1283).
306        let external_ptr = crate::externalptr_derive::derive_external_ptr(
307            derive_input.clone(),
308            attrs.prefer.as_deref() != Some("native"),
309        )?;
310
311        // Apply native preference marker if specified
312        let prefer = if attrs.prefer.as_deref() == Some("native") {
313            crate::list_derive::derive_prefer_rnative(derive_input)?
314        } else {
315            proc_macro2::TokenStream::new()
316        };
317
318        Ok({
    let mut _s = ::quote::__private::TokenStream::new();
    ::quote::ToTokens::to_tokens(&item_ts, &mut _s);
    ::quote::ToTokens::to_tokens(&external_ptr, &mut _s);
    ::quote::ToTokens::to_tokens(&prefer, &mut _s);
    _s
}quote::quote! {
319            #item_ts
320            #external_ptr
321            #prefer
322        })
323    })();
324    result.unwrap_or_else(|e| e.into_compile_error()).into()
325}
326
327/// Dispatches `#[miniextendr]` on a fieldless enum to the correct derive path.
328///
329/// Only C-style (fieldless) enums are supported. Dispatches to:
330/// - `match_arg` mode: `MatchArg` derive (single-selection from R character scalar)
331/// - `factor` mode or default: `RFactor` derive (R factor representation)
332fn expand_enum(
333    attr: proc_macro::TokenStream,
334    item: proc_macro::TokenStream,
335    item_enum: &syn::ItemEnum,
336) -> proc_macro::TokenStream {
337    let attrs = match parse_attrs(attr) {
338        Ok(a) => a,
339        Err(e) => return e.into_compile_error().into(),
340    };
341
342    if !is_fieldless_enum(item_enum) {
343        return syn::Error::new(
344            item_enum.ident.span(),
345            "#[miniextendr] on enums requires all variants to be fieldless (C-style)",
346        )
347        .into_compile_error()
348        .into();
349    }
350
351    let derive_input: syn::DeriveInput = match syn::parse(item.clone()) {
352        Ok(d) => d,
353        Err(e) => return e.into_compile_error().into(),
354    };
355    let derive_input = strip_miniextendr_attrs(derive_input);
356
357    let item_ts: proc_macro2::TokenStream = item.into();
358
359    if attrs.match_arg {
360        // MatchArg mode
361        let result = crate::match_arg_derive::derive_match_arg(derive_input);
362        return match result {
363            Ok(ts) => {
    let mut _s = ::quote::__private::TokenStream::new();
    ::quote::ToTokens::to_tokens(&item_ts, &mut _s);
    ::quote::ToTokens::to_tokens(&ts, &mut _s);
    _s
}quote::quote! { #item_ts #ts }.into(),
364            Err(e) => e.into_compile_error().into(),
365        };
366    }
367
368    if attrs.factor {
369        // Explicit factor mode
370        let result = crate::factor_derive::derive_r_factor(derive_input);
371        return match result {
372            Ok(ts) => {
    let mut _s = ::quote::__private::TokenStream::new();
    ::quote::ToTokens::to_tokens(&item_ts, &mut _s);
    ::quote::ToTokens::to_tokens(&ts, &mut _s);
    _s
}quote::quote! { #item_ts #ts }.into(),
373            Err(e) => e.into_compile_error().into(),
374        };
375    }
376
377    // Default: RFactor
378    let result = crate::factor_derive::derive_r_factor(derive_input);
379    match result {
380        Ok(ts) => {
    let mut _s = ::quote::__private::TokenStream::new();
    ::quote::ToTokens::to_tokens(&item_ts, &mut _s);
    ::quote::ToTokens::to_tokens(&ts, &mut _s);
    _s
}quote::quote! { #item_ts #ts }.into(),
381        Err(e) => e.into_compile_error().into(),
382    }
383}
384
385/// Strip `#[miniextendr(...)]` attributes from a DeriveInput.
386///
387/// Derive helpers don't expect `#[miniextendr]` attributes and may fail or
388/// misinterpret them. We strip them before forwarding.
389fn strip_miniextendr_attrs(mut input: syn::DeriveInput) -> syn::DeriveInput {
390    input
391        .attrs
392        .retain(|attr| !attr.path().is_ident("miniextendr"));
393    input
394}