Skip to main content

miniextendr_macros/
list_derive.rs

1//! # List and Preference Derive Macros
2//!
3//! This module implements derive macros for bidirectional Rust struct <-> R list
4//! conversion, plus "preference" derives that control how a type is converted to R
5//! when returned from `#[miniextendr]` functions.
6//!
7//! ## List Derives
8//!
9//! - `#[derive(IntoList)]` -- Rust struct -> R named/unnamed list
10//! - `#[derive(TryFromList)]` -- R list -> Rust struct
11//!
12//! ## Preference Derives
13//!
14//! These marker derives select the `IntoR` strategy for a type. Only one
15//! preference derive should be applied to a given type:
16//!
17//! - `#[derive(PreferList)]` -- convert via `IntoList::into_list`
18//! - `#[derive(PreferExternalPtr)]` -- wrap in `ExternalPtr::new`
19//! - `#[derive(PreferDataFrame)]` -- convert via `ColumnSource::into_column_list`
20//! - `#[derive(PreferRNativeType)]` -- convert via `AsRNative` wrapper
21//!
22//! Stacking two of them is a conflict: each derive emits a fixed-name marker const
23//! (`prefer_conflict_marker`), so a second `Prefer*` produces a guided
24//! "duplicate definitions" error pointing at the call-site `As*` wrappers as the
25//! way to choose a representation per return value (see #870).
26//!
27//! ## Field Attributes
28//!
29//! - `#[into_list(ignore)]` -- skip this field during IntoList/TryFromList conversion.
30//!   For `TryFromList`, ignored fields are filled with `Default::default()`.
31
32use proc_macro2::TokenStream;
33use quote::quote;
34use syn::{DeriveInput, Fields, parse_quote, spanned::Spanned};
35
36/// Check whether a struct field has the `#[into_list(ignore)]` attribute.
37///
38/// Returns `Ok(true)` if the field should be excluded from list conversion,
39/// or `Err` if an unknown option is found inside `#[into_list(...)]`.
40fn field_is_ignored(field: &syn::Field) -> syn::Result<bool> {
41    let mut ignored = false;
42
43    for attr in &field.attrs {
44        if !attr.path().is_ident("into_list") {
45            continue;
46        }
47
48        attr.parse_nested_meta(|meta| {
49            if meta.path.is_ident("ignore") {
50                ignored = true;
51                return Ok(());
52            }
53
54            Err(meta.error("unknown #[into_list(...)] option; supported: ignore"))
55        })?;
56    }
57
58    Ok(ignored)
59}
60
61/// Derive `IntoList` for structs (Rust -> R).
62///
63/// Generates an `impl IntoList for T` that converts the struct into an R list:
64/// - Named structs (`struct Foo { x: i32 }`) produce a named R list: `list(x = 1L)`
65/// - Tuple structs (`struct Foo(i32, i32)`) produce an unnamed R list: `list(1L, 2L)`
66/// - Unit structs (`struct Foo`) produce an empty R list: `list()`
67///
68/// Fields marked with `#[into_list(ignore)]` are excluded from the list.
69/// Each non-ignored field's type must implement `IntoR` (enforced via where-clause bounds).
70///
71/// Returns `Err` if applied to a non-struct type or if an unknown field attribute is found.
72pub fn derive_into_list(input: DeriveInput) -> syn::Result<TokenStream> {
73    let struct_data = match input.data {
74        syn::Data::Struct(data) => data,
75        _ => {
76            return Err(syn::Error::new(
77                input.ident.span(),
78                "IntoList can only be derived for structs",
79            ));
80        }
81    };
82
83    let name = &input.ident;
84    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
85
86    let mut bounds: Vec<syn::WherePredicate> = Vec::new();
87
88    let (destructure_pat, list_construction) = match &struct_data.fields {
89        // Named struct: create named R list
90        Fields::Named(fields) => {
91            let mut names: Vec<String> = Vec::new();
92            let mut idents: Vec<syn::Ident> = Vec::new();
93
94            for f in fields.named.iter() {
95                let ident = f.ident.as_ref().unwrap().clone();
96                if field_is_ignored(f)? {
97                    continue;
98                }
99                let ty = &f.ty;
100                bounds.push(parse_quote!(#ty: ::miniextendr_api::into_r::IntoR));
101                names.push(ident.to_string());
102                idents.push(ident);
103            }
104
105            let pat = if idents.is_empty() {
106                quote! { { .. } }
107            } else {
108                quote! { { #(#idents),*, .. } }
109            };
110            // Use from_raw_pairs to allow heterogeneous field types.
111            // Each `into_sexp()` is wrapped in `__scope.protect_raw` so prior
112            // field SEXPs survive subsequent allocations — UAF otherwise
113            // (reviews/2026-05-07-gctorture-audit.md).
114            let construction = quote! {
115                // SAFETY: IntoList runs on the R main thread.
116                unsafe {
117                    let __scope = ::miniextendr_api::gc_protect::ProtectScope::new();
118                    ::miniextendr_api::list::List::from_raw_pairs(vec![ #( (#names, __scope.protect_raw(#idents.into_sexp())) ),* ])
119                }
120            };
121            (pat, construction)
122        }
123
124        // Tuple struct: create unnamed R list (positional access)
125        Fields::Unnamed(fields) => {
126            let mut pat_elems: Vec<proc_macro2::TokenStream> = Vec::new();
127            let mut value_idents: Vec<syn::Ident> = Vec::new();
128
129            for (idx, f) in fields.unnamed.iter().enumerate() {
130                if field_is_ignored(f)? {
131                    pat_elems.push(quote! { _ });
132                    continue;
133                }
134                let ident = syn::Ident::new(&format!("_field{idx}"), f.span());
135                let ty = &f.ty;
136                bounds.push(parse_quote!(#ty: ::miniextendr_api::into_r::IntoR));
137                pat_elems.push(quote! { #ident });
138                value_idents.push(ident);
139            }
140
141            let pat = quote! { ( #(#pat_elems),* ) };
142            let construction = quote! {
143                // SAFETY: see above.
144                unsafe {
145                    let __scope = ::miniextendr_api::gc_protect::ProtectScope::new();
146                    ::miniextendr_api::list::List::from_raw_values(vec![ #( __scope.protect_raw(#value_idents.into_sexp()) ),* ])
147                }
148            };
149            (pat, construction)
150        }
151
152        // Unit struct: empty list
153        Fields::Unit => {
154            let pat = quote! {};
155            let construction = quote! {
156                ::miniextendr_api::list::List::from_raw_values(vec![])
157            };
158            (pat, construction)
159        }
160    };
161
162    // Extend where-clause with bounds
163    let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause {
164        where_token: <syn::Token![where]>::default(),
165        predicates: syn::punctuated::Punctuated::new(),
166    });
167    for b in bounds {
168        where_clause.predicates.push(b);
169    }
170
171    let expand = quote! {
172        impl #impl_generics ::miniextendr_api::list::IntoList for #name #ty_generics #where_clause {
173            fn into_list(self) -> ::miniextendr_api::list::List {
174                use ::miniextendr_api::into_r::IntoR;
175                let Self #destructure_pat = self;
176                #list_construction
177            }
178        }
179    };
180
181    Ok(expand)
182}
183
184/// Derive `TryFromList` for structs (R -> Rust).
185///
186/// Generates an `impl TryFromList for T` that extracts struct fields from an R list:
187/// - Named structs: extract by field name from a named R list
188/// - Tuple structs: extract by position (index 0, 1, 2, ...)
189/// - Unit structs: accept any list (no extraction needed)
190///
191/// Fields marked with `#[into_list(ignore)]` are filled with `Default::default()`.
192/// Each non-ignored field's type must implement `TryFromSexp` (enforced via where-clause bounds).
193///
194/// Returns `Err` if applied to a non-struct type or if an unknown field attribute is found.
195pub fn derive_try_from_list(input: DeriveInput) -> syn::Result<TokenStream> {
196    let struct_data = match input.data {
197        syn::Data::Struct(data) => data,
198        _ => {
199            return Err(syn::Error::new(
200                input.ident.span(),
201                "TryFromList can only be derived for structs",
202            ));
203        }
204    };
205
206    let name = &input.ident;
207    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
208
209    let mut bounds: Vec<syn::WherePredicate> = Vec::new();
210
211    let from_list_body = match &struct_data.fields {
212        // Named struct: extract by field name
213        Fields::Named(fields) => {
214            let mut field_extractions: Vec<proc_macro2::TokenStream> = Vec::new();
215            let mut field_inits: Vec<proc_macro2::TokenStream> = Vec::new();
216
217            for f in fields.named.iter() {
218                let ident = f.ident.as_ref().unwrap().clone();
219                let ty = &f.ty;
220
221                if field_is_ignored(f)? {
222                    bounds.push(parse_quote!(#ty: ::core::default::Default));
223                    field_inits.push(quote! { #ident: ::core::default::Default::default() });
224                    continue;
225                }
226
227                // Bound on `TryFromSexp` only — not the associated `Error`
228                // type — and require the field's error convert into `SexpError`
229                // (the trait's error). Pinning `Error = SexpError` rejected any
230                // field whose error is `SexpTypeError` etc. — e.g. `Vec<f64>`
231                // (#861). The `?`/`map_err` below performs the conversion.
232                bounds.push(parse_quote!(#ty: ::miniextendr_api::from_r::TryFromSexp));
233                bounds.push(parse_quote!(::miniextendr_api::from_r::SexpError: ::core::convert::From<<#ty as ::miniextendr_api::from_r::TryFromSexp>::Error>));
234
235                let name_str = ident.to_string();
236                // Fetch the raw element, then convert — so a present-but-wrong-type
237                // field reports the real conversion error instead of being
238                // misreported as a missing field.
239                field_extractions.push(quote! {
240                    let #ident: #ty = {
241                        let __elem = list.get_named_sexp(#name_str)
242                            .ok_or_else(|| ::miniextendr_api::from_r::SexpError::MissingField(#name_str.into()))?;
243                        <#ty as ::miniextendr_api::from_r::TryFromSexp>::try_from_sexp(__elem)
244                            .map_err(::miniextendr_api::from_r::SexpError::from)?
245                    };
246                });
247                field_inits.push(quote! { #ident });
248            }
249
250            quote! {
251                #(#field_extractions)*
252                Ok(Self { #(#field_inits),* })
253            }
254        }
255
256        // Tuple struct: extract by position
257        Fields::Unnamed(fields) => {
258            let mut field_extractions: Vec<proc_macro2::TokenStream> = Vec::new();
259            let mut ctor_args: Vec<proc_macro2::TokenStream> = Vec::new();
260            let mut ignored_fields: Vec<bool> = Vec::with_capacity(fields.unnamed.len());
261            for f in fields.unnamed.iter() {
262                ignored_fields.push(field_is_ignored(f)?);
263            }
264            let input_fields: usize = ignored_fields.iter().filter(|&&b| !b).count();
265            let mut input_idx: usize = 0;
266
267            for (idx, f) in fields.unnamed.iter().enumerate() {
268                let ty = &f.ty;
269
270                if ignored_fields[idx] {
271                    bounds.push(parse_quote!(#ty: ::core::default::Default));
272                    ctor_args.push(quote! { ::core::default::Default::default() });
273                    continue;
274                }
275
276                let ident = syn::Ident::new(&format!("_field{idx}"), f.span());
277                // See the named-struct branch: bound `TryFromSexp` plus a
278                // convertibility bound into `SexpError`, not `Error = SexpError`
279                // (#861).
280                bounds.push(parse_quote!(#ty: ::miniextendr_api::from_r::TryFromSexp));
281                bounds.push(parse_quote!(::miniextendr_api::from_r::SexpError: ::core::convert::From<<#ty as ::miniextendr_api::from_r::TryFromSexp>::Error>));
282
283                let idx_isize = input_idx as isize;
284                field_extractions.push(quote! {
285                    let #ident: #ty = {
286                        let __elem = list.get(#idx_isize)
287                            .ok_or_else(|| ::miniextendr_api::from_r::SexpError::Length(
288                                ::miniextendr_api::from_r::SexpLengthError {
289                                    expected: #input_fields,
290                                    actual: list.len() as usize,
291                                }
292                            ))?;
293                        <#ty as ::miniextendr_api::from_r::TryFromSexp>::try_from_sexp(__elem)
294                            .map_err(::miniextendr_api::from_r::SexpError::from)?
295                    };
296                });
297                ctor_args.push(quote! { #ident });
298                input_idx += 1;
299            }
300
301            quote! {
302                #(#field_extractions)*
303                Ok(Self( #(#ctor_args),* ))
304            }
305        }
306
307        // Unit struct: just return Self
308        Fields::Unit => {
309            quote! { Ok(Self) }
310        }
311    };
312
313    // Extend where-clause with bounds
314    let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause {
315        where_token: <syn::Token![where]>::default(),
316        predicates: syn::punctuated::Punctuated::new(),
317    });
318    for b in bounds {
319        where_clause.predicates.push(b);
320    }
321
322    let expand = quote! {
323        impl #impl_generics ::miniextendr_api::list::TryFromList for #name #ty_generics #where_clause {
324            type Error = ::miniextendr_api::from_r::SexpError;
325
326            fn try_from_list(list: ::miniextendr_api::list::List) -> Result<Self, Self::Error> {
327                #from_list_body
328            }
329        }
330    };
331
332    Ok(expand)
333}
334
335/// Emit a fixed-name conflict marker so that stacking two `Prefer*` derives on a
336/// single type produces a *guided* compile error instead of a cryptic `E0119`
337/// conflicting-`IntoR`-implementation error.
338///
339/// Each `Prefer*` derive declares an inherent associated const with the **same**
340/// self-describing name in an `impl #name` block. A type carries exactly one
341/// representation default, so a second `Prefer*` derive makes rustc report a
342/// `duplicate definitions with name ...` error (E0592) — and the duplicated
343/// identifier itself spells out the fix: pick one type-level default, or choose a
344/// representation per return value at the call site via the `As*` wrappers
345/// (`AsList`, `AsExternalPtr`, `AsDataFrame`, ...).
346///
347/// This fires regardless of derive order and without any cross-derive attribute
348/// inspection — each derive only needs to know its own fixed marker name. (The raw
349/// E0119 on `IntoR` may still co-fire; the duplicate-marker error is the actionable
350/// one because its identifier names both the conflict and the remedy.)
351fn prefer_conflict_marker(input: &DeriveInput) -> TokenStream {
352    let name = &input.ident;
353    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
354
355    quote! {
356        #[allow(non_upper_case_globals, dead_code)]
357        impl #impl_generics #name #ty_generics #where_clause {
358            /// Stacking two `Prefer*` derives on one type is a conflict: a type has
359            /// exactly one `IntoR` default. Keep a single `Prefer*`, or drop them all
360            /// and choose a representation per return value with a call-site `As*`
361            /// wrapper (`AsList`, `AsExternalPtr`, `AsDataFrame`, ...).
362            const __miniextendr_conflicting_Prefer_derives__keep_ONE_or_use_call_site_As_wrappers: () = ();
363        }
364    }
365}
366
367/// Derive `PreferList`: emits an `IntoR` impl that converts to R by first calling
368/// `IntoList::into_list`, then `into_sexp`.
369///
370/// The type must also derive `IntoList` for this to compile. The generated
371/// `IntoR::Error` is `Infallible` (list conversion is infallible for valid structs).
372pub fn derive_prefer_list(input: DeriveInput) -> syn::Result<TokenStream> {
373    let name = &input.ident;
374    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
375    let conflict_marker = prefer_conflict_marker(&input);
376
377    let expand = quote! {
378        impl #impl_generics ::miniextendr_api::into_r::IntoR for #name #ty_generics #where_clause {
379            type Error = std::convert::Infallible;
380
381            #[inline]
382            fn try_into_sexp(self) -> Result<::miniextendr_api::SEXP, Self::Error> {
383                Ok(self.into_sexp())
384            }
385
386            #[inline]
387            unsafe fn try_into_sexp_unchecked(self) -> Result<::miniextendr_api::SEXP, Self::Error> {
388                self.try_into_sexp()
389            }
390
391            #[inline]
392            fn into_sexp(self) -> ::miniextendr_api::SEXP {
393                ::miniextendr_api::list::IntoList::into_list(self).into_sexp()
394            }
395
396            #[inline]
397            unsafe fn into_sexp_unchecked(self) -> ::miniextendr_api::SEXP {
398                ::miniextendr_api::list::IntoList::into_list(self).into_sexp()
399            }
400        }
401
402        #conflict_marker
403    };
404
405    Ok(expand)
406}
407
408/// Derive `PreferExternalPtr`: emits an `IntoR` impl that wraps the value in
409/// `ExternalPtr::new` before converting to SEXP.
410///
411/// The type must implement `TypedExternal` (typically via `#[derive(ExternalPtr)]`).
412/// The generated `IntoR::Error` is `Infallible`.
413pub fn derive_prefer_externalptr(input: DeriveInput) -> syn::Result<TokenStream> {
414    let name = &input.ident;
415    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
416    let conflict_marker = prefer_conflict_marker(&input);
417
418    let expand = quote! {
419        impl #impl_generics ::miniextendr_api::into_r::IntoR for #name #ty_generics #where_clause {
420            type Error = std::convert::Infallible;
421
422            #[inline]
423            fn try_into_sexp(self) -> Result<::miniextendr_api::SEXP, Self::Error> {
424                Ok(self.into_sexp())
425            }
426
427            #[inline]
428            unsafe fn try_into_sexp_unchecked(self) -> Result<::miniextendr_api::SEXP, Self::Error> {
429                self.try_into_sexp()
430            }
431
432            #[inline]
433            fn into_sexp(self) -> ::miniextendr_api::SEXP {
434                ::miniextendr_api::externalptr::ExternalPtr::new(self).into_sexp()
435            }
436
437            #[inline]
438            unsafe fn into_sexp_unchecked(self) -> ::miniextendr_api::SEXP {
439                ::miniextendr_api::externalptr::ExternalPtr::new(self).into_sexp()
440            }
441        }
442
443        #conflict_marker
444    };
445
446    Ok(expand)
447}
448
449/// Derive `PreferDataFrame`: emits an `IntoR` impl that converts to R via
450/// `ColumnSource::into_column_list`, then `into_sexp`.
451///
452/// The type must implement `ColumnSource` (typically the companion struct generated
453/// by `#[derive(DataFrameRow)]`). The generated `IntoR::Error` is `Infallible`.
454pub fn derive_prefer_data_frame(input: DeriveInput) -> syn::Result<TokenStream> {
455    let name = &input.ident;
456    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
457    let conflict_marker = prefer_conflict_marker(&input);
458
459    let expand = quote! {
460        impl #impl_generics ::miniextendr_api::into_r::IntoR for #name #ty_generics #where_clause {
461            type Error = std::convert::Infallible;
462
463            #[inline]
464            fn try_into_sexp(self) -> Result<::miniextendr_api::SEXP, Self::Error> {
465                Ok(self.into_sexp())
466            }
467
468            #[inline]
469            unsafe fn try_into_sexp_unchecked(self) -> Result<::miniextendr_api::SEXP, Self::Error> {
470                self.try_into_sexp()
471            }
472
473            #[inline]
474            fn into_sexp(self) -> ::miniextendr_api::SEXP {
475                ::miniextendr_api::convert::ColumnSource::into_column_list(self).into_sexp()
476            }
477
478            #[inline]
479            unsafe fn into_sexp_unchecked(self) -> ::miniextendr_api::SEXP {
480                ::miniextendr_api::convert::ColumnSource::into_column_list(self).into_sexp()
481            }
482        }
483
484        #conflict_marker
485    };
486
487    Ok(expand)
488}
489
490/// Derive `PreferRNativeType`: emits an `IntoR` impl that wraps the value in
491/// `AsRNative(self)` before calling `IntoR::into_sexp`.
492///
493/// This routes conversion through native R vector allocation, bypassing list/ExternalPtr
494/// paths. The type must also implement `RNativeType` for the `AsRNative` wrapper to compile.
495/// The generated `IntoR::Error` is `Infallible`.
496pub fn derive_prefer_rnative(input: DeriveInput) -> syn::Result<TokenStream> {
497    let name = &input.ident;
498    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
499    let conflict_marker = prefer_conflict_marker(&input);
500
501    let expand = quote! {
502        impl #impl_generics ::miniextendr_api::into_r::IntoR for #name #ty_generics #where_clause {
503            type Error = std::convert::Infallible;
504
505            #[inline]
506            fn try_into_sexp(self) -> Result<::miniextendr_api::SEXP, Self::Error> {
507                Ok(self.into_sexp())
508            }
509
510            #[inline]
511            unsafe fn try_into_sexp_unchecked(self) -> Result<::miniextendr_api::SEXP, Self::Error> {
512                self.try_into_sexp()
513            }
514
515            #[inline]
516            fn into_sexp(self) -> ::miniextendr_api::SEXP {
517                ::miniextendr_api::into_r::IntoR::into_sexp(
518                    ::miniextendr_api::convert::AsRNative(self)
519                )
520            }
521
522            #[inline]
523            unsafe fn into_sexp_unchecked(self) -> ::miniextendr_api::SEXP {
524                ::miniextendr_api::into_r::IntoR::into_sexp_unchecked(
525                    ::miniextendr_api::convert::AsRNative(self)
526                )
527            }
528        }
529
530        #conflict_marker
531    };
532
533    Ok(expand)
534}
535
536/// Derive `PreferVctrs`: emits an `IntoR` impl that converts the type to its R vctrs object
537/// via `IntoVctrs::into_vctrs`.
538///
539/// Used alongside `#[derive(Vctrs)]` (which supplies the `IntoVctrs` impl) so the type can be
540/// returned directly from `#[miniextendr]` functions instead of writing
541/// `value.into_vctrs().map_err(...)` by hand. The generated `IntoR::Error` is
542/// `VctrsBuildError`; a build failure surfaces in R as an error condition.
543#[cfg(feature = "vctrs")]
544pub fn derive_prefer_vctrs(input: DeriveInput) -> syn::Result<TokenStream> {
545    let name = &input.ident;
546    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
547    let conflict_marker = prefer_conflict_marker(&input);
548
549    let expand = quote! {
550        impl #impl_generics ::miniextendr_api::into_r::IntoR for #name #ty_generics #where_clause {
551            type Error = ::miniextendr_api::vctrs::VctrsBuildError;
552
553            #[inline]
554            fn try_into_sexp(self) -> Result<::miniextendr_api::SEXP, Self::Error> {
555                ::miniextendr_api::vctrs::IntoVctrs::into_vctrs(self)
556            }
557
558            #[inline]
559            unsafe fn try_into_sexp_unchecked(self) -> Result<::miniextendr_api::SEXP, Self::Error> {
560                self.try_into_sexp()
561            }
562        }
563
564        #conflict_marker
565    };
566
567    Ok(expand)
568}