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 `IntoDataFrame::into_data_frame`
20//! - `#[derive(PreferRNativeType)]` -- convert via `AsRNative` wrapper
21//!
22//! ## Field Attributes
23//!
24//! - `#[into_list(ignore)]` -- skip this field during IntoList/TryFromList conversion.
25//!   For `TryFromList`, ignored fields are filled with `Default::default()`.
26
27use proc_macro2::TokenStream;
28use quote::quote;
29use syn::{DeriveInput, Fields, parse_quote, spanned::Spanned};
30
31/// Check whether a struct field has the `#[into_list(ignore)]` attribute.
32///
33/// Returns `Ok(true)` if the field should be excluded from list conversion,
34/// or `Err` if an unknown option is found inside `#[into_list(...)]`.
35fn field_is_ignored(field: &syn::Field) -> syn::Result<bool> {
36    let mut ignored = false;
37
38    for attr in &field.attrs {
39        if !attr.path().is_ident("into_list") {
40            continue;
41        }
42
43        attr.parse_nested_meta(|meta| {
44            if meta.path.is_ident("ignore") {
45                ignored = true;
46                return Ok(());
47            }
48
49            Err(meta.error("unknown #[into_list(...)] option; supported: ignore"))
50        })?;
51    }
52
53    Ok(ignored)
54}
55
56/// Derive `IntoList` for structs (Rust -> R).
57///
58/// Generates an `impl IntoList for T` that converts the struct into an R list:
59/// - Named structs (`struct Foo { x: i32 }`) produce a named R list: `list(x = 1L)`
60/// - Tuple structs (`struct Foo(i32, i32)`) produce an unnamed R list: `list(1L, 2L)`
61/// - Unit structs (`struct Foo`) produce an empty R list: `list()`
62///
63/// Fields marked with `#[into_list(ignore)]` are excluded from the list.
64/// Each non-ignored field's type must implement `IntoR` (enforced via where-clause bounds).
65///
66/// Returns `Err` if applied to a non-struct type or if an unknown field attribute is found.
67pub fn derive_into_list(input: DeriveInput) -> syn::Result<TokenStream> {
68    let struct_data = match input.data {
69        syn::Data::Struct(data) => data,
70        _ => {
71            return Err(syn::Error::new(
72                input.ident.span(),
73                "IntoList can only be derived for structs",
74            ));
75        }
76    };
77
78    let name = &input.ident;
79    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
80
81    let mut bounds: Vec<syn::WherePredicate> = Vec::new();
82
83    let (destructure_pat, list_construction) = match &struct_data.fields {
84        // Named struct: create named R list
85        Fields::Named(fields) => {
86            let mut names: Vec<String> = Vec::new();
87            let mut idents: Vec<syn::Ident> = Vec::new();
88
89            for f in fields.named.iter() {
90                let ident = f.ident.as_ref().unwrap().clone();
91                if field_is_ignored(f)? {
92                    continue;
93                }
94                let ty = &f.ty;
95                bounds.push(parse_quote!(#ty: ::miniextendr_api::into_r::IntoR));
96                names.push(ident.to_string());
97                idents.push(ident);
98            }
99
100            let pat = if idents.is_empty() {
101                quote! { { .. } }
102            } else {
103                quote! { { #(#idents),*, .. } }
104            };
105            // Use from_raw_pairs to allow heterogeneous field types.
106            // Each `into_sexp()` is wrapped in `__scope.protect_raw` so prior
107            // field SEXPs survive subsequent allocations — UAF otherwise
108            // (reviews/2026-05-07-gctorture-audit.md).
109            let construction = quote! {
110                // SAFETY: IntoList runs on the R main thread.
111                unsafe {
112                    let __scope = ::miniextendr_api::gc_protect::ProtectScope::new();
113                    ::miniextendr_api::list::List::from_raw_pairs(vec![ #( (#names, __scope.protect_raw(#idents.into_sexp())) ),* ])
114                }
115            };
116            (pat, construction)
117        }
118
119        // Tuple struct: create unnamed R list (positional access)
120        Fields::Unnamed(fields) => {
121            let mut pat_elems: Vec<proc_macro2::TokenStream> = Vec::new();
122            let mut value_idents: Vec<syn::Ident> = Vec::new();
123
124            for (idx, f) in fields.unnamed.iter().enumerate() {
125                if field_is_ignored(f)? {
126                    pat_elems.push(quote! { _ });
127                    continue;
128                }
129                let ident = syn::Ident::new(&format!("_field{idx}"), f.span());
130                let ty = &f.ty;
131                bounds.push(parse_quote!(#ty: ::miniextendr_api::into_r::IntoR));
132                pat_elems.push(quote! { #ident });
133                value_idents.push(ident);
134            }
135
136            let pat = quote! { ( #(#pat_elems),* ) };
137            let construction = quote! {
138                // SAFETY: see above.
139                unsafe {
140                    let __scope = ::miniextendr_api::gc_protect::ProtectScope::new();
141                    ::miniextendr_api::list::List::from_raw_values(vec![ #( __scope.protect_raw(#value_idents.into_sexp()) ),* ])
142                }
143            };
144            (pat, construction)
145        }
146
147        // Unit struct: empty list
148        Fields::Unit => {
149            let pat = quote! {};
150            let construction = quote! {
151                ::miniextendr_api::list::List::from_raw_values(vec![])
152            };
153            (pat, construction)
154        }
155    };
156
157    // Extend where-clause with bounds
158    let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause {
159        where_token: <syn::Token![where]>::default(),
160        predicates: syn::punctuated::Punctuated::new(),
161    });
162    for b in bounds {
163        where_clause.predicates.push(b);
164    }
165
166    let expand = quote! {
167        impl #impl_generics ::miniextendr_api::list::IntoList for #name #ty_generics #where_clause {
168            fn into_list(self) -> ::miniextendr_api::list::List {
169                use ::miniextendr_api::into_r::IntoR;
170                let Self #destructure_pat = self;
171                #list_construction
172            }
173        }
174    };
175
176    Ok(expand)
177}
178
179/// Derive `TryFromList` for structs (R -> Rust).
180///
181/// Generates an `impl TryFromList for T` that extracts struct fields from an R list:
182/// - Named structs: extract by field name from a named R list
183/// - Tuple structs: extract by position (index 0, 1, 2, ...)
184/// - Unit structs: accept any list (no extraction needed)
185///
186/// Fields marked with `#[into_list(ignore)]` are filled with `Default::default()`.
187/// Each non-ignored field's type must implement `TryFromSexp` (enforced via where-clause bounds).
188///
189/// Returns `Err` if applied to a non-struct type or if an unknown field attribute is found.
190pub fn derive_try_from_list(input: DeriveInput) -> syn::Result<TokenStream> {
191    let struct_data = match input.data {
192        syn::Data::Struct(data) => data,
193        _ => {
194            return Err(syn::Error::new(
195                input.ident.span(),
196                "TryFromList can only be derived for structs",
197            ));
198        }
199    };
200
201    let name = &input.ident;
202    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
203
204    let mut bounds: Vec<syn::WherePredicate> = Vec::new();
205
206    let from_list_body = match &struct_data.fields {
207        // Named struct: extract by field name
208        Fields::Named(fields) => {
209            let mut field_extractions: Vec<proc_macro2::TokenStream> = Vec::new();
210            let mut field_inits: Vec<proc_macro2::TokenStream> = Vec::new();
211
212            for f in fields.named.iter() {
213                let ident = f.ident.as_ref().unwrap().clone();
214                let ty = &f.ty;
215
216                if field_is_ignored(f)? {
217                    bounds.push(parse_quote!(#ty: ::core::default::Default));
218                    field_inits.push(quote! { #ident: ::core::default::Default::default() });
219                    continue;
220                }
221
222                bounds.push(parse_quote!(#ty: ::miniextendr_api::from_r::TryFromSexp<Error = ::miniextendr_api::from_r::SexpError>));
223
224                let name_str = ident.to_string();
225                field_extractions.push(quote! {
226                    let #ident: #ty = list.get_named(#name_str)
227                        .ok_or_else(|| ::miniextendr_api::from_r::SexpError::MissingField(#name_str.into()))?;
228                });
229                field_inits.push(quote! { #ident });
230            }
231
232            quote! {
233                #(#field_extractions)*
234                Ok(Self { #(#field_inits),* })
235            }
236        }
237
238        // Tuple struct: extract by position
239        Fields::Unnamed(fields) => {
240            let mut field_extractions: Vec<proc_macro2::TokenStream> = Vec::new();
241            let mut ctor_args: Vec<proc_macro2::TokenStream> = Vec::new();
242            let mut ignored_fields: Vec<bool> = Vec::with_capacity(fields.unnamed.len());
243            for f in fields.unnamed.iter() {
244                ignored_fields.push(field_is_ignored(f)?);
245            }
246            let input_fields: usize = ignored_fields.iter().filter(|&&b| !b).count();
247            let mut input_idx: usize = 0;
248
249            for (idx, f) in fields.unnamed.iter().enumerate() {
250                let ty = &f.ty;
251
252                if ignored_fields[idx] {
253                    bounds.push(parse_quote!(#ty: ::core::default::Default));
254                    ctor_args.push(quote! { ::core::default::Default::default() });
255                    continue;
256                }
257
258                let ident = syn::Ident::new(&format!("_field{idx}"), f.span());
259                bounds.push(parse_quote!(#ty: ::miniextendr_api::from_r::TryFromSexp<Error = ::miniextendr_api::from_r::SexpError>));
260
261                let idx_isize = input_idx as isize;
262                field_extractions.push(quote! {
263                    let #ident: #ty = list.get_index(#idx_isize)
264                        .ok_or_else(|| ::miniextendr_api::from_r::SexpError::Length(
265                            ::miniextendr_api::from_r::SexpLengthError {
266                                expected: #input_fields,
267                                actual: list.len() as usize,
268                            }
269                        ))?;
270                });
271                ctor_args.push(quote! { #ident });
272                input_idx += 1;
273            }
274
275            quote! {
276                #(#field_extractions)*
277                Ok(Self( #(#ctor_args),* ))
278            }
279        }
280
281        // Unit struct: just return Self
282        Fields::Unit => {
283            quote! { Ok(Self) }
284        }
285    };
286
287    // Extend where-clause with bounds
288    let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause {
289        where_token: <syn::Token![where]>::default(),
290        predicates: syn::punctuated::Punctuated::new(),
291    });
292    for b in bounds {
293        where_clause.predicates.push(b);
294    }
295
296    let expand = quote! {
297        impl #impl_generics ::miniextendr_api::list::TryFromList for #name #ty_generics #where_clause {
298            type Error = ::miniextendr_api::from_r::SexpError;
299
300            fn try_from_list(list: ::miniextendr_api::list::List) -> Result<Self, Self::Error> {
301                #from_list_body
302            }
303        }
304    };
305
306    Ok(expand)
307}
308
309/// Derive `PreferList`: adds the `PrefersList` marker trait and an `IntoR` impl
310/// that converts to R by first calling `IntoList::into_list`, then `into_sexp`.
311///
312/// The type must also derive `IntoList` for this to compile. The generated
313/// `IntoR::Error` is `Infallible` (list conversion is infallible for valid structs).
314pub fn derive_prefer_list(input: DeriveInput) -> syn::Result<TokenStream> {
315    let name = &input.ident;
316    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
317
318    let expand = quote! {
319        impl #impl_generics ::miniextendr_api::markers::PrefersList for #name #ty_generics #where_clause {}
320
321        impl #impl_generics ::miniextendr_api::into_r::IntoR for #name #ty_generics #where_clause {
322            type Error = std::convert::Infallible;
323
324            #[inline]
325            fn try_into_sexp(self) -> Result<::miniextendr_api::ffi::SEXP, Self::Error> {
326                Ok(self.into_sexp())
327            }
328
329            #[inline]
330            unsafe fn try_into_sexp_unchecked(self) -> Result<::miniextendr_api::ffi::SEXP, Self::Error> {
331                self.try_into_sexp()
332            }
333
334            #[inline]
335            fn into_sexp(self) -> ::miniextendr_api::ffi::SEXP {
336                ::miniextendr_api::list::IntoList::into_list(self).into_sexp()
337            }
338
339            #[inline]
340            unsafe fn into_sexp_unchecked(self) -> ::miniextendr_api::ffi::SEXP {
341                ::miniextendr_api::list::IntoList::into_list(self).into_sexp()
342            }
343        }
344    };
345
346    Ok(expand)
347}
348
349/// Derive `PreferExternalPtr`: adds the `PrefersExternalPtr` marker trait and an
350/// `IntoR` impl that wraps the value in `ExternalPtr::new` before converting to SEXP.
351///
352/// The type must implement `TypedExternal` (typically via `#[derive(ExternalPtr)]`).
353/// The generated `IntoR::Error` is `Infallible`.
354pub fn derive_prefer_externalptr(input: DeriveInput) -> syn::Result<TokenStream> {
355    let name = &input.ident;
356    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
357
358    let expand = quote! {
359        impl #impl_generics ::miniextendr_api::markers::PrefersExternalPtr for #name #ty_generics #where_clause {}
360
361        impl #impl_generics ::miniextendr_api::into_r::IntoR for #name #ty_generics #where_clause {
362            type Error = std::convert::Infallible;
363
364            #[inline]
365            fn try_into_sexp(self) -> Result<::miniextendr_api::ffi::SEXP, Self::Error> {
366                Ok(self.into_sexp())
367            }
368
369            #[inline]
370            unsafe fn try_into_sexp_unchecked(self) -> Result<::miniextendr_api::ffi::SEXP, Self::Error> {
371                self.try_into_sexp()
372            }
373
374            #[inline]
375            fn into_sexp(self) -> ::miniextendr_api::ffi::SEXP {
376                ::miniextendr_api::externalptr::ExternalPtr::new(self).into_sexp()
377            }
378
379            #[inline]
380            unsafe fn into_sexp_unchecked(self) -> ::miniextendr_api::ffi::SEXP {
381                ::miniextendr_api::externalptr::ExternalPtr::new(self).into_sexp()
382            }
383        }
384    };
385
386    Ok(expand)
387}
388
389/// Derive `PreferDataFrame`: adds the `PrefersDataFrame` marker trait and an
390/// `IntoR` impl that converts to R via `IntoDataFrame::into_data_frame`, then `into_sexp`.
391///
392/// The type must implement `IntoDataFrame` (typically the companion struct generated
393/// by `#[derive(DataFrameRow)]`). The generated `IntoR::Error` is `Infallible`.
394pub fn derive_prefer_data_frame(input: DeriveInput) -> syn::Result<TokenStream> {
395    let name = &input.ident;
396    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
397
398    let expand = quote! {
399        impl #impl_generics ::miniextendr_api::markers::PrefersDataFrame for #name #ty_generics #where_clause {}
400
401        impl #impl_generics ::miniextendr_api::into_r::IntoR for #name #ty_generics #where_clause {
402            type Error = std::convert::Infallible;
403
404            #[inline]
405            fn try_into_sexp(self) -> Result<::miniextendr_api::ffi::SEXP, Self::Error> {
406                Ok(self.into_sexp())
407            }
408
409            #[inline]
410            unsafe fn try_into_sexp_unchecked(self) -> Result<::miniextendr_api::ffi::SEXP, Self::Error> {
411                self.try_into_sexp()
412            }
413
414            #[inline]
415            fn into_sexp(self) -> ::miniextendr_api::ffi::SEXP {
416                ::miniextendr_api::convert::IntoDataFrame::into_data_frame(self).into_sexp()
417            }
418
419            #[inline]
420            unsafe fn into_sexp_unchecked(self) -> ::miniextendr_api::ffi::SEXP {
421                ::miniextendr_api::convert::IntoDataFrame::into_data_frame(self).into_sexp()
422            }
423        }
424    };
425
426    Ok(expand)
427}
428
429/// Derive `PreferRNativeType`: adds the `PrefersRNativeType` marker trait and an
430/// `IntoR` impl that wraps the value in `AsRNative(self)` before calling `IntoR::into_sexp`.
431///
432/// This routes conversion through native R vector allocation, bypassing list/ExternalPtr
433/// paths. The type must also implement `RNativeType` for the `AsRNative` wrapper to compile.
434/// The generated `IntoR::Error` is `Infallible`.
435pub fn derive_prefer_rnative(input: DeriveInput) -> syn::Result<TokenStream> {
436    let name = &input.ident;
437    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
438
439    let expand = quote! {
440        impl #impl_generics ::miniextendr_api::markers::PrefersRNativeType for #name #ty_generics #where_clause {}
441
442        impl #impl_generics ::miniextendr_api::into_r::IntoR for #name #ty_generics #where_clause {
443            type Error = std::convert::Infallible;
444
445            #[inline]
446            fn try_into_sexp(self) -> Result<::miniextendr_api::ffi::SEXP, Self::Error> {
447                Ok(self.into_sexp())
448            }
449
450            #[inline]
451            unsafe fn try_into_sexp_unchecked(self) -> Result<::miniextendr_api::ffi::SEXP, Self::Error> {
452                self.try_into_sexp()
453            }
454
455            #[inline]
456            fn into_sexp(self) -> ::miniextendr_api::ffi::SEXP {
457                ::miniextendr_api::into_r::IntoR::into_sexp(
458                    ::miniextendr_api::convert::AsRNative(self)
459                )
460            }
461
462            #[inline]
463            unsafe fn into_sexp_unchecked(self) -> ::miniextendr_api::ffi::SEXP {
464                ::miniextendr_api::into_r::IntoR::into_sexp_unchecked(
465                    ::miniextendr_api::convert::AsRNative(self)
466                )
467            }
468        }
469    };
470
471    Ok(expand)
472}