Skip to main content

miniextendr_macros/
externalptr_derive.rs

1//! # `#[derive(ExternalPtr)]` - ExternalPtr Support
2//!
3//! This module implements the `#[derive(ExternalPtr)]` macro which generates
4//! a `TypedExternal` impl for use with `ExternalPtr<T>`.
5//!
6//! Trait ABI wrapper infrastructure is automatically generated when you use
7//! `#[miniextendr]` on `impl Trait for Type` blocks.
8//!
9//! ## Sidecar codegen vs `#[miniextendr]` fns
10//!
11//! `#[derive(ExternalPtr)]` with `#[r_data]` fields emits a pair of C/R
12//! wrappers per sidecar field (`<Type>_get_<field>` /
13//! `<Type>_set_<field>`). **These wrappers do NOT go through
14//! `c_wrapper_builder::CWrapperContext`** — they are hand-rolled in this
15//! module so they can present the simplest signature R expects from a slot
16//! accessor:
17//!
18//! | Aspect | `#[miniextendr]` fn / method (`c_wrapper_builder`) | Sidecar accessor (here) |
19//! |---|---|---|
20//! | First C param | `__miniextendr_call: SEXP` | none |
21//! | C `numArgs` | 1 + user args | `1` (getter) / `2` (setter) |
22//! | R `.Call` | `.Call(C_…, .call = match.call(), …)` | `.Call(C_…, x)` / `.Call(C_…, x, value)` |
23//! | Error transport | tagged-condition SEXP via `with_r_unwind_protect(…, Some(call))` | tagged-condition SEXP via `with_r_unwind_protect(…, None)`; the R guard's `sys.call()` supplies attribution |
24//!
25//! **Do not add `.call = match.call()` to a sidecar R wrapper.** The C
26//! function doesn't have a slot for it — R will throw "Incorrect number of
27//! arguments" at runtime. The two paths drifting apart is tracked in #348.
28//!
29//! ## Usage
30//!
31//! ### Basic (no traits)
32//!
33//! ```ignore
34//! #[derive(ExternalPtr)]
35//! struct MyData {
36//!     value: i32,
37//! }
38//! // Generates: impl TypedExternal for MyData { ... }
39//! ```
40//!
41//! ### With R Sidecar Slots and Class System
42//!
43//! The `#[r_data]` attribute marks fields for R-side storage. Use `#[externalptr(...)]`
44//! to specify a class system for appropriate R wrapper generation:
45//!
46//! | Class System | Attribute | R Accessors |
47//! |--------------|-----------|-------------|
48//! | Environment | `#[externalptr(env)]` (default) | `Type_get_field()`, `Type_set_field()` |
49//! | R6 | `#[externalptr(r6)]` | Active bindings in R6Class |
50//! | S3 | `#[externalptr(s3)]` | `$.class`, `$<-.class` methods |
51//! | S4 | `#[externalptr(s4)]` | Slot accessors |
52//! | S7 | `#[externalptr(s7)]` | Properties via `new_property()` |
53//!
54//! Three field tiers are supported:
55//!
56//! 1. **Raw SEXP** (`SEXP`) - Direct SEXP access, no conversion
57//! 2. **Zero-overhead scalars** (`i32`, `f64`, `bool`, `u8`) - Direct R memory access
58//! 3. **Conversion types** (anything else) - Uses `IntoR`/`TryFromSexp` traits
59//!
60//! ```ignore
61//! #[derive(ExternalPtr)]
62//! #[externalptr(r6)]  // R6 class - generates active bindings
63//! pub struct MyType {
64//!     pub x: i32,
65//!
66//!     #[r_data]
67//!     r: RSidecar,  // Selector - enables R accessors for this type
68//!
69//!     #[r_data]
70//!     pub raw_slot: SEXP,  // Raw SEXP, no conversion
71//!
72//!     #[r_data]
73//!     pub count: i32,  // Zero-overhead: stored as R INTEGER(1)
74//!
75//!     #[r_data]
76//!     pub score: f64,  // Zero-overhead: stored as R REAL(1)
77//!
78//!     #[r_data]
79//!     pub name: String,  // Conversion: uses IntoR/TryFromSexp
80//! }
81//! // Generates: active bindings `count`, `score`, `name` in R6Class
82//! ```
83//!
84//! ### Trait ABI wiring
85//!
86//! ```ignore
87//! #[derive(ExternalPtr)]
88//! struct MyCounter {
89//!     value: i32,
90//! }
91//!
92//! #[miniextendr]
93//! impl Counter for MyCounter { /* ... */ }
94//! ```
95//!
96//! ## Generated Types (trait impls)
97//!
98//! ### Wrapper Struct
99//!
100//! ```ignore
101//! #[repr(C)]
102//! struct __MxWrapperMyCounter {
103//!     erased: mx_erased,  // Must be first field
104//!     data: MyCounter,
105//! }
106//! ```
107//!
108//! ### Base Vtable
109//!
110//! ```ignore
111//! static __MX_BASE_VTABLE_MYCOUNTER: mx_base_vtable = mx_base_vtable {
112//!     drop: __mx_drop_mycounter,
113//!     concrete_tag: TAG_MYCOUNTER,
114//!     query: __mx_query_mycounter,
115//! };
116//! ```
117//!
118//! ### Query Function
119//!
120//! The query function maps trait tags to vtable pointers:
121//!
122//! ```ignore
123//! unsafe extern "C" fn __mx_query_mycounter(
124//!     ptr: *mut mx_erased,
125//!     trait_tag: mx_tag,
126//! ) -> *const c_void {
127//!     if trait_tag == TAG_COUNTER {
128//!         return std::ptr::from_ref(&__VTABLE_MYPKG_COUNTER_FOR_MYCOUNTER).cast::<c_void>();
129//!     }
130//!     std::ptr::null()
131//! }
132//! ```
133
134use proc_macro2::{Span, TokenStream};
135use syn::{DeriveInput, Field, Ident, Visibility};
136
137use crate::miniextendr_impl::ClassSystem;
138
139/// Parse `#[externalptr(...)]` attributes to extract class system.
140///
141/// Supported forms:
142/// - `#[externalptr(env)]` - Environment style (default)
143/// - `#[externalptr(r6)]` - R6 class
144/// - `#[externalptr(s3)]` - S3 class
145/// - `#[externalptr(s4)]` - S4 class
146/// - `#[externalptr(s7)]` - S7 class
147fn parse_externalptr_attrs(input: &DeriveInput) -> syn::Result<ClassSystem> {
148    let mut class_system = ClassSystem::Env; // Default
149
150    for attr in &input.attrs {
151        if attr.path().is_ident("externalptr") {
152            attr.parse_nested_meta(|meta| {
153                let ident_str = meta
154                    .path
155                    .get_ident()
156                    .map(|i| i.to_string())
157                    .unwrap_or_default();
158
159                match ident_str.as_str() {
160                    "env" => class_system = ClassSystem::Env,
161                    "r6" => class_system = ClassSystem::R6,
162                    "s3" => class_system = ClassSystem::S3,
163                    "s4" => class_system = ClassSystem::S4,
164                    "s7" => class_system = ClassSystem::S7,
165                    "vctrs" => class_system = ClassSystem::Vctrs,
166                    _ => {
167                        return Err(syn::Error::new_spanned(
168                            &meta.path,
169                            format!(
170                                "unknown class system '{}'; expected one of: env, r6, s3, s4, s7, vctrs",
171                                ident_str
172                            ),
173                        ));
174                    }
175                }
176                Ok(())
177            })?;
178        }
179    }
180
181    Ok(class_system)
182}
183
184/// Check if a field has the `#[r_data]` attribute.
185fn has_r_data_attr(field: &Field) -> bool {
186    field.attrs.iter().any(|a| a.path().is_ident("r_data"))
187}
188
189/// Parse `prop_doc = "..."` from an `#[r_data(prop_doc = "...")]` attribute.
190///
191/// Returns `None` if the field has no `#[r_data]` attribute, no `prop_doc` key,
192/// or if the attribute has no parenthesized arguments (bare `#[r_data]`).
193fn parse_r_data_prop_doc(field: &Field) -> syn::Result<Option<String>> {
194    for attr in &field.attrs {
195        if !attr.path().is_ident("r_data") {
196            continue;
197        }
198        // `#[r_data]` with no arguments — no prop_doc
199        if matches!(attr.meta, syn::Meta::Path(_)) {
200            return Ok(None);
201        }
202        let mut prop_doc = None;
203        attr.parse_nested_meta(|meta| {
204            if meta.path.is_ident("prop_doc") {
205                let value = meta.value()?;
206                let lit: syn::LitStr = value.parse()?;
207                prop_doc = Some(lit.value());
208                Ok(())
209            } else {
210                Err(meta.error(format!(
211                    "unknown key `{}`; supported: `prop_doc`",
212                    meta.path
213                        .get_ident()
214                        .map(|i| i.to_string())
215                        .unwrap_or_default()
216                )))
217            }
218        })?;
219        return Ok(prop_doc);
220    }
221    Ok(None)
222}
223
224/// Check if a field type is `RSidecar`.
225///
226/// Returns `true` if the last path segment of the field's type is `RSidecar`,
227/// which acts as the selector marker enabling R sidecar accessor generation.
228fn is_rsidecar_type(field: &Field) -> bool {
229    if let syn::Type::Path(type_path) = &field.ty {
230        type_path
231            .path
232            .segments
233            .last()
234            .map(|seg| seg.ident == "RSidecar")
235            .unwrap_or(false)
236    } else {
237        false
238    }
239}
240
241/// Check if a field is public.
242fn is_pub(field: &Field) -> bool {
243    matches!(field.vis, Visibility::Public(_))
244}
245
246/// The kind of sidecar slot, determining how getter/setter FFI functions are generated.
247///
248/// Each kind maps to a different codegen strategy for reading from and writing to
249/// the Rust struct through R's `.Call` interface:
250/// - Raw SEXP: no conversion, direct pass-through.
251/// - Zero-overhead scalars: use R's `Rf_Scalar*`/`Rf_as*` for single-element coercion.
252/// - Conversion: use the `IntoR`/`TryFromSexp` traits for arbitrary types.
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254enum SlotKind {
255    /// SEXP type -- raw SEXP access (getter returns SEXP, setter takes SEXP).
256    RawSexp,
257    /// Zero-overhead scalar: `i32` (or `i16`/`i8`) stored as `INTEGER(1)`.
258    ScalarInt,
259    /// Zero-overhead scalar: `f64` (or `f32`) stored as `REAL(1)`.
260    ScalarReal,
261    /// Zero-overhead scalar: `bool` (or `Rbool`) stored as `LOGICAL(1)`.
262    ScalarLogical,
263    /// Zero-overhead scalar: `u8` stored as `RAW(1)`.
264    ScalarRaw,
265    /// Conversion type -- uses `IntoR`/`TryFromSexp` traits for arbitrary Rust types.
266    Conversion,
267}
268
269/// Information about a single `#[r_data]`-annotated sidecar slot field.
270///
271/// Collected during struct field parsing and used to generate FFI getter/setter
272/// functions and R wrapper code for each public slot.
273struct SidecarSlot {
274    /// Rust identifier of the field (e.g., `count`, `name`).
275    name: Ident,
276    /// Rust type of the field, used in conversion-based getter/setter codegen.
277    ty: syn::Type,
278    /// Zero-based index of this slot in the protection VECSXP
279    /// (offset from `PROT_BASE_LEN`, which reserves slots for type ID and user data).
280    index: usize,
281    /// Whether the field is `pub`. Only public fields get R accessor functions.
282    is_public: bool,
283    /// Determines the codegen strategy for reading/writing this slot.
284    kind: SlotKind,
285    /// Optional documentation string for the S7 `@prop` tag.
286    /// Sourced from `#[r_data(prop_doc = "...")]`. `None` means no doc was supplied;
287    /// a default fallback string is used at emit time.
288    prop_doc: Option<String>,
289}
290
291/// Aggregated sidecar information extracted from struct field analysis.
292///
293/// Contains everything needed to generate sidecar accessor code: the
294/// selector presence, the list of typed slots, and the target class system.
295struct SidecarInfo {
296    /// Whether the struct contains an `RSidecar`-typed field marked with `#[r_data]`.
297    /// At most one selector is allowed per struct.
298    has_selector: bool,
299    /// The `#[r_data]` slot fields (excluding the RSidecar selector itself),
300    /// each carrying its index, kind, and visibility.
301    slots: Vec<SidecarSlot>,
302    /// The R class system chosen via `#[externalptr(...)]`, controlling the
303    /// style of generated R wrapper code.
304    class_system: ClassSystem,
305}
306
307/// Determine the [`SlotKind`] for a field type by inspecting its last path segment.
308///
309/// Recognizes `SEXP`, scalar numerics (`i32`, `i16`, `i8`, `f64`, `f32`),
310/// booleans (`bool`, `Rbool`), and raw bytes (`u8`). Everything else falls
311/// through to [`SlotKind::Conversion`].
312fn slot_kind_for_type(ty: &syn::Type) -> SlotKind {
313    if let syn::Type::Path(type_path) = ty
314        && let Some(seg) = type_path.path.segments.last()
315    {
316        let ident = &seg.ident;
317        // Check for raw SEXP access
318        if ident == "SEXP" {
319            return SlotKind::RawSexp;
320        }
321        // Check for zero-overhead scalar types
322        if ident == "i32" || ident == "i16" || ident == "i8" {
323            return SlotKind::ScalarInt;
324        }
325        if ident == "f64" || ident == "f32" {
326            return SlotKind::ScalarReal;
327        }
328        if ident == "bool" || ident == "Rbool" {
329            return SlotKind::ScalarLogical;
330        }
331        if ident == "u8" {
332            return SlotKind::ScalarRaw;
333        }
334    }
335    // Everything else uses conversion
336    SlotKind::Conversion
337}
338
339/// Parse struct fields for sidecar information.
340///
341/// Iterates over all fields, identifying `#[r_data]` markers. Fields with
342/// `RSidecar` type are tracked as selector markers (at most one allowed);
343/// all other `#[r_data]` fields become [`SidecarSlot`] entries with their
344/// slot kind inferred from the field type.
345///
346/// Returns `Err` if more than one `RSidecar` field is found.
347fn parse_sidecar_info(input: &DeriveInput, class_system: ClassSystem) -> syn::Result<SidecarInfo> {
348    let fields = match &input.data {
349        syn::Data::Struct(data) => &data.fields,
350        _ => {
351            return Ok(SidecarInfo {
352                has_selector: false,
353                slots: vec![],
354                class_system,
355            });
356        }
357    };
358
359    let mut selector_fields: Vec<&Field> = vec![];
360    let mut slots = vec![];
361    let mut slot_index = 0usize;
362
363    for field in fields.iter() {
364        if !has_r_data_attr(field) {
365            continue;
366        }
367
368        if is_rsidecar_type(field) {
369            // RSidecar is the selector marker, not a slot
370            selector_fields.push(field);
371        } else if let Some(ref ident) = field.ident {
372            // Any other type with #[r_data] becomes a slot
373            let kind = slot_kind_for_type(&field.ty);
374            let prop_doc = parse_r_data_prop_doc(field)?;
375            slots.push(SidecarSlot {
376                name: ident.clone(),
377                ty: field.ty.clone(),
378                index: slot_index,
379                is_public: is_pub(field),
380                kind,
381                prop_doc,
382            });
383            slot_index += 1;
384        }
385    }
386
387    // Check for multiple selectors
388    if selector_fields.len() > 1 {
389        return Err(syn::Error::new_spanned(
390            selector_fields[1],
391            "only one RSidecar field is allowed per struct",
392        ));
393    }
394
395    Ok(SidecarInfo {
396        has_selector: !selector_fields.is_empty(),
397        slots,
398        class_system,
399    })
400}
401
402/// Generate the token stream for a sidecar getter function body.
403///
404/// Reads the field value from the Rust struct (accessed via the external
405/// pointer address) and converts it to an R SEXP. The conversion strategy
406/// depends on the slot kind:
407/// - `RawSexp`: returns the SEXP field directly.
408/// - Scalar kinds: wraps in `Rf_Scalar*` for zero-overhead conversion.
409/// - `Conversion`: clones the value and calls `IntoR::into_sexp`.
410///
411/// The body runs inside `with_r_unwind_protect` (see the emission site in
412/// [`generate_sidecar_accessors`]); a non-external-pointer argument, a null
413/// pointer address, or a wrong stored type panics with the same
414/// `expected ExternalPtr<T>` message the main class-method path uses. The
415/// panic is transported as a tagged condition and re-raised by the R wrapper.
416fn generate_getter_body(
417    struct_name: &syn::Ident,
418    slot: &SidecarSlot,
419    _prot_index_lit: &syn::LitInt,
420) -> TokenStream {
421    let field_name = &slot.name;
422
423    // Helper: generate the pointer extraction code for Box<dyn Any> storage.
424    // R_ExternalPtrAddr returns *mut Box<dyn Any>; we downcast to &T.
425    // Failure modes panic (caught by the surrounding with_r_unwind_protect
426    // and raised as an R error) instead of silently returning R_NilValue.
427    let extract_ref = quote::quote! {
428        use ::miniextendr_api::{SEXP, SexpExt};
429        use ::miniextendr_api::sys::R_ExternalPtrAddr;
430        if x.type_of() != ::miniextendr_api::SEXPTYPE::EXTPTRSXP {
431            ::std::panic!(concat!(
432                "expected ExternalPtr<", stringify!(#struct_name),
433                ">, got a non-external-pointer object"
434            ));
435        }
436        let any_raw = R_ExternalPtrAddr(x) as *mut Box<dyn ::std::any::Any>;
437        if any_raw.is_null() {
438            ::std::panic!(concat!(
439                "expected ExternalPtr<", stringify!(#struct_name),
440                ">, got a null external pointer"
441            ));
442        }
443        let any_box: &Box<dyn ::std::any::Any> = &*any_raw;
444        let data: &#struct_name = any_box
445            .downcast_ref::<#struct_name>()
446            .expect(concat!("expected ExternalPtr<", stringify!(#struct_name), ">"));
447    };
448
449    match slot.kind {
450        SlotKind::RawSexp => {
451            // Raw SEXP field - return directly (already an R value)
452            quote::quote! {
453                unsafe {
454                    #extract_ref
455                    data.#field_name
456                }
457            }
458        }
459        SlotKind::ScalarInt => {
460            quote::quote! {
461                use ::miniextendr_api::SEXP;
462                unsafe {
463                    #extract_ref
464                    SEXP::scalar_integer(data.#field_name)
465                }
466            }
467        }
468        SlotKind::ScalarReal => {
469            quote::quote! {
470                use ::miniextendr_api::SEXP;
471                unsafe {
472                    #extract_ref
473                    SEXP::scalar_real(data.#field_name)
474                }
475            }
476        }
477        SlotKind::ScalarLogical => {
478            quote::quote! {
479                use ::miniextendr_api::SEXP;
480                unsafe {
481                    #extract_ref
482                    SEXP::scalar_logical(data.#field_name)
483                }
484            }
485        }
486        SlotKind::ScalarRaw => {
487            quote::quote! {
488                use ::miniextendr_api::SEXP;
489                unsafe {
490                    #extract_ref
491                    SEXP::scalar_raw(data.#field_name)
492                }
493            }
494        }
495        SlotKind::Conversion => {
496            // Use IntoR trait for conversion (e.g., String -> character)
497            let ty = &slot.ty;
498            quote::quote! {
499                use ::miniextendr_api::into_r::IntoR;
500                unsafe {
501                    #extract_ref
502                    let val: #ty = data.#field_name.clone();
503                    <#ty as IntoR>::into_sexp(val)
504                }
505            }
506        }
507    }
508}
509
510/// Generate the token stream for a sidecar setter function body.
511///
512/// Converts the incoming R SEXP `value` and writes it to the corresponding
513/// Rust struct field. The conversion strategy depends on the slot kind:
514/// - `RawSexp`: stores the SEXP directly.
515/// - Scalar kinds: uses `Rf_as*` or coercion for single-element extraction;
516///   an input that doesn't reduce to a single non-NA scalar raises a
517///   conversion error instead of silently storing an NA/`false` sentinel.
518/// - `Conversion`: uses `TryFromSexp::try_from_sexp`; a failed conversion
519///   raises a conversion error instead of silently dropping the write.
520///
521/// Returns the external pointer `x` (for R's invisible return convention).
522/// The body runs inside `with_r_unwind_protect` (see the emission site in
523/// [`generate_sidecar_accessors`]); a bad receiver panics with the main-path
524/// `expected ExternalPtr<T>` message and conversion failures return a tagged
525/// condition SEXP with kind `conversion` — both are re-raised by the R
526/// wrapper. Sidecar accessors have no `__miniextendr_call` slot (#344/#348),
527/// so the tagged conditions carry null call attribution.
528fn generate_setter_body(
529    struct_name: &syn::Ident,
530    slot: &SidecarSlot,
531    _prot_index_lit: &syn::LitInt,
532) -> TokenStream {
533    let field_name = &slot.name;
534
535    // Helper: extract mutable reference via Box<dyn Any> downcast.
536    // Failure modes panic (caught by the surrounding with_r_unwind_protect
537    // and raised as an R error) instead of silently no-op'ing.
538    let extract_mut = quote::quote! {
539        use ::miniextendr_api::SexpExt;
540        use ::miniextendr_api::sys::R_ExternalPtrAddr;
541        if x.type_of() != ::miniextendr_api::SEXPTYPE::EXTPTRSXP {
542            ::std::panic!(concat!(
543                "expected ExternalPtr<", stringify!(#struct_name),
544                ">, got a non-external-pointer object"
545            ));
546        }
547        let any_raw = R_ExternalPtrAddr(x) as *mut Box<dyn ::std::any::Any>;
548        if any_raw.is_null() {
549            ::std::panic!(concat!(
550                "expected ExternalPtr<", stringify!(#struct_name),
551                ">, got a null external pointer"
552            ));
553        }
554        let any_box: &mut Box<dyn ::std::any::Any> = &mut *any_raw;
555        let data = any_box
556            .downcast_mut::<#struct_name>()
557            .expect(concat!("expected ExternalPtr<", stringify!(#struct_name), ">"));
558    };
559
560    // Conversion-failure error message prefix, mirroring the main path's
561    // "failed to convert parameter '<name>' to <ty>: ..." wording.
562    let err_prefix = format!(
563        "failed to convert value for sidecar field '{}' on `{}`",
564        field_name, struct_name
565    );
566    let scalar_err = |expected: &str| -> syn::LitStr {
567        syn::LitStr::new(
568            &format!("{err_prefix}: expected {expected}"),
569            field_name.span(),
570        )
571    };
572
573    match slot.kind {
574        SlotKind::RawSexp => {
575            quote::quote! {
576                unsafe {
577                    #extract_mut
578                    data.#field_name = value;
579                    x
580                }
581            }
582        }
583        SlotKind::ScalarInt => {
584            let err_msg = scalar_err("a single non-NA integer-compatible value");
585            quote::quote! {
586                use ::miniextendr_api::SexpExt;
587                unsafe {
588                    #extract_mut
589                    data.#field_name = match value.as_integer() {
590                        Some(v) => v,
591                        None => return ::miniextendr_api::error_value::make_rust_condition_value(
592                            #err_msg,
593                            ::miniextendr_api::error_value::kind::CONVERSION,
594                            ::core::option::Option::None,
595                            ::core::option::Option::None,
596                        ),
597                    };
598                    x
599                }
600            }
601        }
602        SlotKind::ScalarReal => {
603            let err_msg = scalar_err("a single non-NA numeric-compatible value");
604            quote::quote! {
605                use ::miniextendr_api::SexpExt;
606                unsafe {
607                    #extract_mut
608                    data.#field_name = match value.as_real() {
609                        Some(v) => v,
610                        None => return ::miniextendr_api::error_value::make_rust_condition_value(
611                            #err_msg,
612                            ::miniextendr_api::error_value::kind::CONVERSION,
613                            ::core::option::Option::None,
614                            ::core::option::Option::None,
615                        ),
616                    };
617                    x
618                }
619            }
620        }
621        SlotKind::ScalarLogical => {
622            let err_msg = scalar_err("a single non-NA logical-compatible value");
623            quote::quote! {
624                use ::miniextendr_api::SexpExt;
625                unsafe {
626                    #extract_mut
627                    data.#field_name = match value.as_logical() {
628                        Some(v) => v,
629                        None => return ::miniextendr_api::error_value::make_rust_condition_value(
630                            #err_msg,
631                            ::miniextendr_api::error_value::kind::CONVERSION,
632                            ::core::option::Option::None,
633                            ::core::option::Option::None,
634                        ),
635                    };
636                    x
637                }
638            }
639        }
640        SlotKind::ScalarRaw => {
641            let err_msg = scalar_err("at least one raw-compatible value");
642            quote::quote! {
643                use ::miniextendr_api::{SexpExt, SEXPTYPE};
644                unsafe {
645                    #extract_mut
646                    let raw_vec = value.coerce(SEXPTYPE::RAWSXP);
647                    if raw_vec.len() == 0 {
648                        return ::miniextendr_api::error_value::make_rust_condition_value(
649                            #err_msg,
650                            ::miniextendr_api::error_value::kind::CONVERSION,
651                            ::core::option::Option::None,
652                            ::core::option::Option::None,
653                        );
654                    }
655                    data.#field_name = raw_vec.raw_elt(0);
656                    x
657                }
658            }
659        }
660        SlotKind::Conversion => {
661            let ty = &slot.ty;
662            let err_msg_lit = syn::LitStr::new(&err_prefix, field_name.span());
663            quote::quote! {
664                use ::miniextendr_api::TryFromSexp;
665                unsafe {
666                    #extract_mut
667                    data.#field_name = match <#ty as TryFromSexp>::try_from_sexp(value) {
668                        Ok(val) => val,
669                        Err(e) => return ::miniextendr_api::error_value::make_rust_condition_value(
670                            &format!("{}: {e}", #err_msg_lit),
671                            ::miniextendr_api::error_value::kind::CONVERSION,
672                            ::core::option::Option::None,
673                            ::core::option::Option::None,
674                        ),
675                    };
676                    x
677                }
678            }
679        }
680    }
681}
682
683/// Generate R wrapper code (roxygen-annotated R functions) for a single sidecar slot.
684///
685/// Produces getter and setter R functions that call the corresponding C entry
686/// points via `.Call`. The generated R code includes roxygen tags (`@rdname`,
687/// `@param`, `@return`, `@export`) for documentation.
688///
689/// All class systems generate the same standalone function pattern
690/// (`Type_get_field` / `Type_set_field`); only the roxygen title suffix
691/// differs. Class-specific integration (e.g., R6 active bindings, S7
692/// properties) is handled separately by [`generate_class_integration_r_code`].
693///
694/// Each wrapper body carries the shared tagged-condition guard
695/// ([`crate::method_return_builder::standalone_body`]) so Rust-origin
696/// panics/conversion failures are re-raised as structured R conditions.
697/// Note the sidecar C functions have **no** `.call` slot — the `.Call()` must
698/// not pass `.call = match.call()` (#344/#348); `sys.call()` inside the guard
699/// supplies fallback attribution instead.
700fn generate_r_wrapper_for_slot(
701    class_system: ClassSystem,
702    type_name: &str,
703    field_name: &str,
704    getter_c_name: &str,
705    setter_c_name: &str,
706) -> String {
707    // Only the roxygen title suffix differs between class systems.
708    let suffix = match class_system {
709        ClassSystem::Env => "",
710        ClassSystem::R6 => " (for R6)",
711        ClassSystem::S3 => " (for S3)",
712        ClassSystem::S4 => " (for S4)",
713        ClassSystem::S7 => " (for S7)",
714        ClassSystem::Vctrs => " (for vctrs)",
715    };
716    let r_getter_name = format!("{}_get_{}", type_name, field_name);
717    let r_setter_name = format!("{}_set_{}", type_name, field_name);
718    let getter_body = crate::method_return_builder::standalone_body(
719        &format!(".Call({getter_c_name}, x)"),
720        ".val",
721        "  ",
722    );
723    let setter_body = crate::method_return_builder::standalone_body(
724        &format!(".Call({setter_c_name}, x, value)"),
725        "invisible(x)",
726        "  ",
727    );
728    format!(
729        r#"
730#' Get `{field}` field from {type}{suffix}
731#' @rdname {type}
732#' @param x The {type} external pointer
733#' @return The value of the `{field}` field
734#' @export
735{r_getter} <- function(x) {{
736  {getter_body}
737}}
738
739#' Set `{field}` field on {type}{suffix}
740#' @rdname {type}
741#' @param x The {type} external pointer
742#' @param value The new value to set
743#' @return The {type} pointer (invisibly)
744#' @export
745{r_setter} <- function(x, value) {{
746  {setter_body}
747}}
748"#,
749        type = type_name,
750        field = field_name,
751        suffix = suffix,
752        r_getter = r_getter_name,
753        r_setter = r_setter_name,
754        getter_body = getter_body,
755        setter_body = setter_body,
756    )
757}
758
759/// Generate class-integrated R code for sidecar fields.
760///
761/// For R6: generates `Type$set("active", "field", ...)` calls that add active bindings
762/// referencing the sidecar getter/setter .Call entrypoints.
763///
764/// For S7: generates `.rdata_properties_Type <- list(...)` with S7::new_property()
765/// definitions that can be spliced into the S7 class's properties list.
766///
767/// Other class systems return empty strings (their standalone accessors suffice).
768fn generate_class_integration_r_code(
769    class_system: ClassSystem,
770    type_name: &str,
771    pub_slots: &[&SidecarSlot],
772) -> String {
773    if pub_slots.is_empty() {
774        return String::new();
775    }
776
777    // Shared tagged-condition guard (one line); `sys.call()` supplies fallback
778    // attribution because the sidecar C functions carry no `.call` slot
779    // (#344/#348 — never add `.call = match.call()` to a sidecar `.Call()`).
780    let guard =
781        |indent: &str| crate::method_return_builder::condition_check_lines(indent).join("\n");
782
783    match class_system {
784        ClassSystem::R6 => {
785            // Generate $set("active", ...) calls for each sidecar field.
786            // These are appended after the R6Class definition and add active bindings
787            // that delegate to the sidecar .Call accessors.
788            let mut code = String::new();
789            code.push_str(&format!(
790                "\n# Auto-generated active bindings for {type} sidecar fields.\n",
791                type = type_name,
792            ));
793            code.push_str(
794                "# These are applied when `r_data_accessors` is set on the impl block.\n",
795            );
796            code.push_str(&format!(
797                ".rdata_active_bindings_{type} <- function(cls) {{\n\
798                 \x20 # R CMD check: self/private are R6 runtime bindings (set by cls$set)\n\
799                 \x20 self <- private <- NULL\n",
800                type = type_name,
801            ));
802            for slot in pub_slots {
803                let field = slot.name.to_string();
804                // Must match the sidecar accessor's actual C symbol exactly (#1273
805                // crate-prefixing) — routed through the shared naming.rs helpers.
806                let getter_c = crate::naming::sidecar_getter_c_name(type_name, &field);
807                let setter_c = crate::naming::sidecar_setter_c_name(type_name, &field);
808                code.push_str(&format!(
809                    "  cls$set(\"active\", \"{field}\", function(value) {{\n\
810                     \x20   if (missing(value)) {{\n\
811                     \x20     .val <- .Call({getter_c}, private$.ptr)\n\
812                     {getter_guard}\n\
813                     \x20     .val\n\
814                     \x20   }} else {{\n\
815                     \x20     .val <- .Call({setter_c}, private$.ptr, value)\n\
816                     {setter_guard}\n\
817                     \x20     invisible(self)\n\
818                     \x20   }}\n\
819                     \x20 }}, overwrite = TRUE)\n",
820                    field = field,
821                    getter_c = getter_c,
822                    setter_c = setter_c,
823                    getter_guard = guard("      "),
824                    setter_guard = guard("      "),
825                ));
826            }
827            code.push_str("}\n");
828            code
829        }
830        ClassSystem::S7 => {
831            // Generate a helper list of S7::new_property() definitions.
832            // The S7 wrapper generator references this when `r_data_accessors` is set.
833            let mut code = String::new();
834            code.push_str(&format!(
835                "\n# Auto-generated S7 property definitions for {type} sidecar fields.\n",
836                type = type_name,
837            ));
838            code.push_str(&format!(
839                ".rdata_properties_{type} <- list(\n",
840                type = type_name,
841            ));
842            for (i, slot) in pub_slots.iter().enumerate() {
843                let field = slot.name.to_string();
844                // Must match the sidecar accessor's actual C symbol exactly (#1273
845                // crate-prefixing) — routed through the shared naming.rs helpers.
846                let getter_c = crate::naming::sidecar_getter_c_name(type_name, &field);
847                let setter_c = crate::naming::sidecar_setter_c_name(type_name, &field);
848                let comma = if i < pub_slots.len() - 1 { "," } else { "" };
849                code.push_str(&format!(
850                    "    {field} = S7::new_property(\n\
851                     \x20       getter = function(self) {{\n\
852                     \x20         .val <- .Call({getter_c}, self@.ptr)\n\
853                     {getter_guard}\n\
854                     \x20         .val\n\
855                     \x20       }},\n\
856                     \x20       setter = function(self, value) {{\n\
857                     \x20         .val <- .Call({setter_c}, self@.ptr, value)\n\
858                     {setter_guard}\n\
859                     \x20         self\n\
860                     \x20       }}\n\
861                     \x20   ){comma}\n",
862                    field = field,
863                    getter_c = getter_c,
864                    setter_c = setter_c,
865                    getter_guard = guard("          "),
866                    setter_guard = guard("          "),
867                    comma = comma,
868                ));
869            }
870            code.push_str(")\n");
871            code
872        }
873        // Other class systems don't need class-integrated code
874        _ => String::new(),
875    }
876}
877
878/// Generate sidecar accessor constants and `extern "C-unwind"` functions.
879///
880/// For each public `#[r_data]` field, generates:
881/// - A getter FFI function (`C_<crate>__mx_rdata_get_Type_field`)
882/// - A setter FFI function (`C_<crate>__mx_rdata_set_Type_field`)
883/// - `R_CallMethodDef` entries for routine registration
884/// - R wrapper function code (roxygen-documented)
885/// - Class-integration code for R6 / S7 (active bindings / properties)
886///
887/// The generated constants are:
888/// - `RDATA_CALL_DEFS_{TYPE}`: slice of `R_CallMethodDef` for registration
889/// - `R_WRAPPERS_RDATA_{TYPE}`: string literal of R wrapper code
890///
891/// Returns `Err` if the struct has generic type parameters (`.Call` entrypoints
892/// cannot be generic).
893fn generate_sidecar_accessors(input: &DeriveInput, info: &SidecarInfo) -> syn::Result<TokenStream> {
894    // Reject generic structs — .Call entrypoints cannot be generic
895    if !input.generics.params.is_empty() {
896        return Err(syn::Error::new_spanned(
897            &input.generics,
898            "ExternalPtr does not support generic structs; \
899             .Call entrypoints cannot be generic",
900        ));
901    }
902
903    // If no selector or no public slots, nothing to register
904    let pub_slots: Vec<_> = info.slots.iter().filter(|s| s.is_public).collect();
905    if !info.has_selector || pub_slots.is_empty() {
906        return Ok(quote::quote! {});
907    }
908
909    let name = &input.ident;
910    let name_str = name.to_string();
911    let name_upper = name_str.to_uppercase();
912
913    // Base prot slot indices (type ID at 0, user at 1)
914    const PROT_BASE_LEN: usize = 2;
915
916    // Generate getter/setter functions and R wrappers for each pub slot
917    let mut c_functions = vec![];
918    let mut r_wrappers = String::new();
919
920    // Add documentation header that establishes the @name topic for sidecar accessors.
921    // This ensures @rdname references have a valid target even if the main class
922    // definition uses @noRd.
923    if !pub_slots.is_empty() {
924        r_wrappers.push_str(&format!(
925            r#"
926#' @title {type} Sidecar Accessors
927#' @name {type}
928#' @description Getter and setter functions for `#[r_data]` fields on `{type}`.
929#' @source Generated by miniextendr from `#[derive(ExternalPtr)]` on `{type}`
930NULL
931
932"#,
933            type = name_str,
934        ));
935    }
936
937    for slot in &pub_slots {
938        let field_name = &slot.name;
939        let field_name_str = field_name.to_string();
940        let prot_index = PROT_BASE_LEN + slot.index;
941
942        // C function names (crate-prefixed for webR cross-package symbol
943        // uniqueness — #1273, routed through the shared naming.rs helpers)
944        let getter_c_name = crate::naming::sidecar_getter_c_name(&name_str, &field_name_str);
945        let setter_c_name = crate::naming::sidecar_setter_c_name(&name_str, &field_name_str);
946        let getter_fn_name = Ident::new(&getter_c_name, Span::call_site());
947        let setter_fn_name = Ident::new(&setter_c_name, Span::call_site());
948        let source_location_doc = crate::source_location_doc(field_name.span());
949        let getter_doc = format!(
950            "Generated sidecar getter for `{}` field on Rust type `{}`.",
951            field_name_str, name_str
952        );
953        let setter_doc = format!(
954            "Generated sidecar setter for `{}` field on Rust type `{}`.",
955            field_name_str, name_str
956        );
957        let getter_doc_lit = syn::LitStr::new(&getter_doc, field_name.span());
958        let setter_doc_lit = syn::LitStr::new(&setter_doc, field_name.span());
959
960        let prot_index_lit = syn::LitInt::new(&prot_index.to_string(), Span::call_site());
961
962        // Generate getter/setter bodies based on slot kind
963        let getter_body = generate_getter_body(name, slot, &prot_index_lit);
964        let setter_body = generate_setter_body(name, slot, &prot_index_lit);
965
966        // Generate C getter function.
967        //
968        // The body runs under `with_r_unwind_protect` so Rust panics (and R
969        // longjmps during allocation) become a tagged condition SEXP that the
970        // R wrapper re-raises, matching the main call-slot path. Sidecar
971        // accessors have no `__miniextendr_call` slot (#344/#348), so the
972        // transport uses null call attribution; the R wrapper's guard passes
973        // `sys.call()` to `.miniextendr_raise_condition` as the fallback.
974        c_functions.push(quote::quote! {
975            #[doc = #getter_doc_lit]
976            #[doc = #source_location_doc]
977            #[doc = concat!("Generated from source file `", file!(), "`.")]
978            #[doc(hidden)]
979            #[unsafe(no_mangle)]
980            pub unsafe extern "C-unwind" fn #getter_fn_name(
981                x: ::miniextendr_api::SEXP
982            ) -> ::miniextendr_api::SEXP {
983                ::miniextendr_api::unwind_protect::with_r_unwind_protect(
984                    || { #getter_body },
985                    ::core::option::Option::None,
986                )
987            }
988        });
989
990        // Generate C setter function (same unwind/condition transport as the
991        // getter above).
992        c_functions.push(quote::quote! {
993            #[doc = #setter_doc_lit]
994            #[doc = #source_location_doc]
995            #[doc = concat!("Generated from source file `", file!(), "`.")]
996            #[doc(hidden)]
997            #[unsafe(no_mangle)]
998            pub unsafe extern "C-unwind" fn #setter_fn_name(
999                x: ::miniextendr_api::SEXP,
1000                value: ::miniextendr_api::SEXP,
1001            ) -> ::miniextendr_api::SEXP {
1002                ::miniextendr_api::unwind_protect::with_r_unwind_protect(
1003                    || { #setter_body },
1004                    ::core::option::Option::None,
1005                )
1006            }
1007        });
1008
1009        // Generate R_CallMethodDef entries via distributed slice
1010        let getter_c_name_cstr = format!("{}\0", getter_c_name);
1011        let setter_c_name_cstr = format!("{}\0", setter_c_name);
1012        let getter_cstr_lit =
1013            syn::LitByteStr::new(getter_c_name_cstr.as_bytes(), Span::call_site());
1014        let setter_cstr_lit =
1015            syn::LitByteStr::new(setter_c_name_cstr.as_bytes(), Span::call_site());
1016        let getter_def_ident = Ident::new(
1017            &format!(
1018                "__MX_CALL_DEF_RDATA_GET_{}_{}",
1019                name_upper,
1020                field_name_str.to_uppercase()
1021            ),
1022            Span::call_site(),
1023        );
1024        let setter_def_ident = Ident::new(
1025            &format!(
1026                "__MX_CALL_DEF_RDATA_SET_{}_{}",
1027                name_upper,
1028                field_name_str.to_uppercase()
1029            ),
1030            Span::call_site(),
1031        );
1032
1033        c_functions.push(quote::quote! {
1034            #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_CALL_DEFS), linkme(crate = ::miniextendr_api::linkme))]
1035            #[doc(hidden)]
1036            static #getter_def_ident: ::miniextendr_api::sys::R_CallMethodDef =
1037                ::miniextendr_api::sys::R_CallMethodDef {
1038                    name: #getter_cstr_lit.as_ptr().cast(),
1039                    fun: Some(unsafe { ::std::mem::transmute(#getter_fn_name as unsafe extern "C-unwind" fn(_) -> _) }),
1040                    numArgs: 1,
1041                };
1042        });
1043        c_functions.push(quote::quote! {
1044            #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_CALL_DEFS), linkme(crate = ::miniextendr_api::linkme))]
1045            #[doc(hidden)]
1046            static #setter_def_ident: ::miniextendr_api::sys::R_CallMethodDef =
1047                ::miniextendr_api::sys::R_CallMethodDef {
1048                    name: #setter_cstr_lit.as_ptr().cast(),
1049                    fun: Some(unsafe { ::std::mem::transmute(#setter_fn_name as unsafe extern "C-unwind" fn(_, _) -> _) }),
1050                    numArgs: 2,
1051                };
1052        });
1053
1054        // Generate R wrapper code based on class system
1055        let field_start = field_name.span().start();
1056        r_wrappers.push_str(&format!(
1057            "# Generated from Rust source line {}:{}\n# Wraps sidecar field `{}` on Rust type `{}` via `{}` and `{}`.\n",
1058            field_start.line,
1059            field_start.column + 1,
1060            field_name_str,
1061            name_str,
1062            getter_c_name,
1063            setter_c_name,
1064        ));
1065        r_wrappers.push_str(&generate_r_wrapper_for_slot(
1066            info.class_system,
1067            &name_str,
1068            &field_name_str,
1069            &getter_c_name,
1070            &setter_c_name,
1071        ));
1072    }
1073
1074    // Generate class-integrated R code for R6 and S7.
1075    // This code is appended after the standalone accessors so that
1076    // `r_data_accessors` in the impl block can auto-integrate sidecar fields.
1077    r_wrappers.push_str(&generate_class_integration_r_code(
1078        info.class_system,
1079        &name_str,
1080        &pub_slots,
1081    ));
1082
1083    let const_name_wrappers = Ident::new(
1084        &format!("R_WRAPPERS_RDATA_{}", name_upper),
1085        Span::call_site(),
1086    );
1087    let source_location_doc = crate::source_location_doc(name.span());
1088
1089    // For S7 class systems, emit MX_S7_SIDECAR_PROPS entries so the S7 codegen
1090    // can substitute @prop lines for sidecar properties at write time.
1091    let sidecar_prop_entries = if info.class_system == ClassSystem::S7 {
1092        let entries: Vec<_> = pub_slots
1093            .iter()
1094            .map(|slot| {
1095                let field_str = slot.name.to_string();
1096                let doc_str = slot
1097                    .prop_doc
1098                    .as_deref()
1099                    .unwrap_or("(undocumented sidecar property)");
1100                let entry_ident = Ident::new(
1101                    &format!(
1102                        "__MX_S7_SIDECAR_PROP_{}_{}",
1103                        name_upper,
1104                        field_str.to_uppercase()
1105                    ),
1106                    Span::call_site(),
1107                );
1108                quote::quote! {
1109                    #[doc(hidden)]
1110                    #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_S7_SIDECAR_PROPS), linkme(crate = ::miniextendr_api::linkme))]
1111                    static #entry_ident: ::miniextendr_api::registry::SidecarPropEntry =
1112                        ::miniextendr_api::registry::SidecarPropEntry {
1113                            rust_type: #name_str,
1114                            field_name: #field_str,
1115                            prop_doc: #doc_str,
1116                        };
1117                }
1118            })
1119            .collect();
1120        quote::quote! { #(#entries)* }
1121    } else {
1122        quote::quote! {}
1123    };
1124
1125    Ok(quote::quote! {
1126        #(#c_functions)*
1127
1128        /// Sidecar accessor R wrapper code via distributed slice.
1129        #[doc = #source_location_doc]
1130        #[doc = concat!("Generated from source file `", file!(), "`.")]
1131        #[doc(hidden)]
1132        #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_R_WRAPPERS), linkme(crate = ::miniextendr_api::linkme))]
1133        static #const_name_wrappers: ::miniextendr_api::registry::RWrapperEntry =
1134            ::miniextendr_api::registry::RWrapperEntry {
1135                priority: ::miniextendr_api::registry::RWrapperPriority::Sidecar,
1136                source_file: file!(),
1137                content: #r_wrappers,
1138            };
1139
1140        #sidecar_prop_entries
1141    })
1142}
1143
1144/// Generate the `TypedExternal` trait implementation for the derive target.
1145///
1146/// Produces three associated constants:
1147/// - `TYPE_NAME`: the struct name as a `&'static str`
1148/// - `TYPE_NAME_CSTR`: null-terminated byte string of the struct name
1149/// - `TYPE_ID_CSTR`: globally unique ID in the format
1150///   `"<crate_name>@<crate_version>::<module_path>::<type_name>\0"`,
1151///   using `CARGO_PKG_NAME`, `CARGO_PKG_VERSION`, and `module_path!()`.
1152///
1153/// Supports generic structs (generics are forwarded to the impl).
1154fn generate_typed_external(input: &DeriveInput) -> TokenStream {
1155    let name = &input.ident;
1156    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1157
1158    let name_str = name.to_string();
1159    let name_lit = syn::LitStr::new(&name_str, name.span());
1160    let name_cstr = syn::LitByteStr::new(format!("{}\0", name_str).as_bytes(), name.span());
1161
1162    // TYPE_ID_CSTR format: "<crate_name>@<crate_version>::<module_path>::<type_name>\0"
1163    //
1164    // Uses env!("CARGO_PKG_NAME") and env!("CARGO_PKG_VERSION") for the crate identifier,
1165    // ensuring two packages with the same type name from the same crate+version are compatible,
1166    // while different crate versions are considered distinct types.
1167    //
1168    // The module_path!() may include "crate::" prefix when compiled within the crate,
1169    // but combined with the explicit crate@version prefix, this is unambiguous.
1170    quote::quote! {
1171        impl #impl_generics ::miniextendr_api::externalptr::TypedExternal for #name #ty_generics #where_clause {
1172            const TYPE_NAME: &'static str = #name_lit;
1173            const TYPE_NAME_CSTR: &'static [u8] = #name_cstr;
1174            const TYPE_ID_CSTR: &'static [u8] =
1175                concat!(
1176                    env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION"),
1177                    "::", module_path!(), "::", #name_lit, "\0"
1178                ).as_bytes();
1179        }
1180    }
1181}
1182
1183/// Generate the `IntoExternalPtr` marker trait impl.
1184///
1185/// This marker trait enables the blanket `impl<T: IntoExternalPtr> IntoR for T`
1186/// in miniextendr-api, allowing the type to be returned directly from functions.
1187fn generate_into_external_ptr(input: &DeriveInput) -> TokenStream {
1188    let name = &input.ident;
1189    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1190
1191    quote::quote! {
1192        impl #impl_generics ::miniextendr_api::externalptr::IntoExternalPtr for #name #ty_generics #where_clause {}
1193    }
1194}
1195
1196/// Generate the concrete `IntoRVecElement` impl (issue #1284).
1197///
1198/// Lights up the single `impl<T: IntoRVecElement> IntoR for Vec<T>` blanket in
1199/// miniextendr-api, so `-> Vec<MyType>` returns compile and produce a `VECSXP`
1200/// of external pointers. The impl must be concrete (not a blanket over
1201/// `IntoExternalPtr`) because the `MatchArg` bridge already occupies a blanket
1202/// on `IntoRVecElement` and two blankets would collide (E0119) — the same
1203/// funnel design `#[derive(IntoR)]` newtypes use (see
1204/// `miniextendr_api::newtype` module docs).
1205///
1206/// GC discipline: `ExternalPtr::collect_into_r_list` allocates each
1207/// `EXTPTRSXP` directly into the already-protected destination list, so no
1208/// element is ever live-but-unrooted across a later allocation.
1209fn generate_into_r_vec_element(input: &DeriveInput) -> TokenStream {
1210    let name = &input.ident;
1211    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1212
1213    quote::quote! {
1214        impl #impl_generics ::miniextendr_api::IntoRVecElement for #name #ty_generics #where_clause {
1215            #[inline]
1216            fn elements_into_sexp(values: ::std::vec::Vec<Self>) -> ::miniextendr_api::SEXP {
1217                ::miniextendr_api::externalptr::ExternalPtr::<Self>::collect_into_r_list(values)
1218            }
1219        }
1220    }
1221}
1222
1223/// Main entry point for `#[derive(ExternalPtr)]`.
1224///
1225/// Orchestrates the full derive expansion:
1226/// 1. Parses `#[externalptr(...)]` attributes for class system selection.
1227/// 2. Analyzes struct fields for `#[r_data]` sidecar slots.
1228/// 3. Generates `TypedExternal` impl (type identity for `ExternalPtr<T>`).
1229/// 4. Generates `IntoExternalPtr` marker impl (enables `IntoR` blanket impl)
1230///    and a concrete `IntoRVecElement` impl (enables the `IntoR for Vec<T>`
1231///    blanket, so `-> Vec<MyType>` returns a `VECSXP` of external pointers —
1232///    #1284). Both are suppressed when `emit_into_r_marker` is `false`, which
1233///    the struct-level `#[miniextendr(prefer = "native")]` path uses so its
1234///    concrete `AsRNative` `IntoR` does not collide with the blanket
1235///    `IntoExternalPtr` `IntoR` (E0119, #1283).
1236/// 5. Generates sidecar accessor FFI functions, registration constants, and R wrappers.
1237///
1238/// Returns the combined token stream of all generated items.
1239pub fn derive_external_ptr(
1240    input: DeriveInput,
1241    emit_into_r_marker: bool,
1242) -> syn::Result<TokenStream> {
1243    // Parse class system from #[externalptr(...)] attribute
1244    let class_system = parse_externalptr_attrs(&input)?;
1245
1246    // Parse sidecar information from struct fields
1247    let sidecar_info = parse_sidecar_info(&input, class_system)?;
1248
1249    let typed_external = generate_typed_external(&input);
1250    let (into_external_ptr, into_r_vec_element) = if emit_into_r_marker {
1251        (
1252            generate_into_external_ptr(&input),
1253            generate_into_r_vec_element(&input),
1254        )
1255    } else {
1256        (
1257            proc_macro2::TokenStream::new(),
1258            proc_macro2::TokenStream::new(),
1259        )
1260    };
1261    let sidecar_accessors = generate_sidecar_accessors(&input, &sidecar_info)?;
1262    let erased_wrapper = generate_erased_wrapper(&input);
1263
1264    Ok(quote::quote! {
1265        #typed_external
1266        #into_external_ptr
1267        #into_r_vec_element
1268        #sidecar_accessors
1269        #erased_wrapper
1270    })
1271}
1272
1273/// Generate the type-erased wrapper infrastructure for trait ABI dispatch.
1274///
1275/// For `#[derive(ExternalPtr)] struct MyCounter { ... }` generates:
1276/// - `__MxWrapperMyCounter` - repr(C) wrapper with mx_erased header + data
1277/// - `__MX_TAG_MYCOUNTER` - concrete type tag (FNV-1a hash of module_path + type)
1278/// - `__mx_drop_mycounter` - destructor for R GC
1279/// - `__MX_BASE_VTABLE_MYCOUNTER` - base vtable with universal_query
1280/// - `__mx_wrap_mycounter` - constructor returning `*mut mx_erased`
1281fn generate_erased_wrapper(input: &DeriveInput) -> TokenStream {
1282    let type_ident = &input.ident;
1283
1284    // Only for non-generic types (generics can't participate in trait dispatch)
1285    if !input.generics.params.is_empty() {
1286        return quote::quote! {};
1287    }
1288
1289    let type_upper = type_ident.to_string().to_uppercase();
1290    let type_lower = type_ident.to_string().to_lowercase();
1291
1292    let wrapper_name = quote::format_ident!("__MxWrapper{}", type_ident);
1293    let base_vtable_name = quote::format_ident!("__MX_BASE_VTABLE_{}", type_upper);
1294    let concrete_tag_name = quote::format_ident!("__MX_TAG_{}", type_upper);
1295    let drop_fn_name = quote::format_ident!("__mx_drop_{}", type_lower);
1296    let wrap_fn_name = quote::format_ident!("__mx_wrap_{}", type_lower);
1297    let source_loc_doc = crate::source_location_doc(type_ident.span());
1298    let tag_path = format!("::{}", type_ident);
1299
1300    quote::quote! {
1301        #[doc = concat!(
1302            "Type-erased wrapper for `",
1303            stringify!(#type_ident),
1304            "` with trait dispatch support."
1305        )]
1306        #[doc = "Generated by `#[derive(ExternalPtr)]`."]
1307        #[doc = #source_loc_doc]
1308        #[doc = concat!("Generated from source file `", file!(), "`.")]
1309        #[repr(C)]
1310        #[doc(hidden)]
1311        struct #wrapper_name {
1312            pub erased: ::miniextendr_api::abi::mx_erased,
1313            pub data: #type_ident,
1314        }
1315
1316        #[doc(hidden)]
1317        const #concrete_tag_name: ::miniextendr_api::abi::mx_tag =
1318            ::miniextendr_api::abi::mx_tag_from_path(concat!(module_path!(), #tag_path));
1319
1320        #[doc(hidden)]
1321        unsafe extern "C" fn #drop_fn_name(ptr: *mut ::miniextendr_api::abi::mx_erased) {
1322            if ptr.is_null() {
1323                return;
1324            }
1325            let wrapper = ptr.cast::<#wrapper_name>();
1326            // A panicking Drop impl must not unwind across the C-ABI boundary.
1327            // `drop_catching_panic` catches any panic and aborts instead.
1328            ::miniextendr_api::externalptr::drop_catching_panic(|| {
1329                unsafe { drop(Box::from_raw(wrapper)); }
1330            });
1331        }
1332
1333        #[doc(hidden)]
1334        static #base_vtable_name: ::miniextendr_api::abi::mx_base_vtable =
1335            ::miniextendr_api::abi::mx_base_vtable {
1336                drop: #drop_fn_name,
1337                concrete_tag: #concrete_tag_name,
1338                query: ::miniextendr_api::registry::universal_query,
1339                data_offset: ::std::mem::offset_of!(#wrapper_name, data),
1340            };
1341
1342        #[doc(hidden)]
1343        fn #wrap_fn_name(data: #type_ident) -> *mut ::miniextendr_api::abi::mx_erased {
1344            let wrapper = Box::new(#wrapper_name {
1345                erased: ::miniextendr_api::abi::mx_erased {
1346                    base: &#base_vtable_name,
1347                },
1348                data,
1349            });
1350            Box::into_raw(wrapper).cast::<::miniextendr_api::abi::mx_erased>()
1351        }
1352    }
1353}
1354
1355#[cfg(test)]
1356mod tests {
1357    use super::{ClassSystem, generate_class_integration_r_code, generate_r_wrapper_for_slot};
1358
1359    const ALL_CLASS_SYSTEMS: [ClassSystem; 6] = [
1360        ClassSystem::Env,
1361        ClassSystem::R6,
1362        ClassSystem::S3,
1363        ClassSystem::S4,
1364        ClassSystem::S7,
1365        ClassSystem::Vctrs,
1366    ];
1367
1368    /// Sidecar accessor C functions take only `x` (getter) or `x, value` (setter) —
1369    /// no `__miniextendr_call` parameter. The R wrappers must NOT pass `.call = match.call()`
1370    /// because that would be counted as an extra positional argument by `.Call()`, causing
1371    /// "Incorrect number of arguments" errors at runtime. Cover every class system variant.
1372    #[test]
1373    fn sidecar_accessors_do_not_pass_match_call() {
1374        let getter_c = "C__mx_rdata_get_T_f";
1375        let setter_c = "C__mx_rdata_set_T_f";
1376
1377        for cs in ALL_CLASS_SYSTEMS {
1378            let out = generate_r_wrapper_for_slot(cs, "T", "f", getter_c, setter_c);
1379            // Correct form: no .call argument — the C function only accepts x (getter) or x, value (setter).
1380            assert!(
1381                out.contains(&format!(".Call({getter_c}, x)")),
1382                "{cs:?} getter should call without .call:\n{out}"
1383            );
1384            assert!(
1385                out.contains(&format!(".Call({setter_c}, x, value)")),
1386                "{cs:?} setter should call without .call:\n{out}"
1387            );
1388            // Must NOT include the erroneous .call = match.call() form.
1389            assert!(
1390                !out.contains(".call = match.call()"),
1391                "{cs:?} sidecar wrapper must not pass .call = match.call():\n{out}"
1392            );
1393        }
1394    }
1395
1396    /// Every sidecar R wrapper (getter and setter, all class systems) must
1397    /// carry the tagged-condition guard so Rust panics / conversion failures
1398    /// transported by `with_r_unwind_protect(…, None)` are re-raised as
1399    /// structured R conditions instead of leaking the tagged SEXP to the user.
1400    #[test]
1401    fn sidecar_accessors_reraise_tagged_conditions() {
1402        let getter_c = "C__mx_rdata_get_T_f";
1403        let setter_c = "C__mx_rdata_set_T_f";
1404
1405        for cs in ALL_CLASS_SYSTEMS {
1406            let out = generate_r_wrapper_for_slot(cs, "T", "f", getter_c, setter_c);
1407            assert_eq!(
1408                out.matches(".miniextendr_raise_condition(.val, sys.call())")
1409                    .count(),
1410                2,
1411                "{cs:?} getter+setter should each carry the condition guard:\n{out}"
1412            );
1413        }
1414    }
1415
1416    /// The R6 active-binding and S7 property integration code also call the
1417    /// sidecar C entrypoints directly; they need the same guard (with
1418    /// `sys.call()` fallback attribution) and must not pass `.call =`.
1419    #[test]
1420    fn sidecar_class_integration_reraises_tagged_conditions() {
1421        let slot = super::SidecarSlot {
1422            name: syn::Ident::new("f", proc_macro2::Span::call_site()),
1423            ty: syn::parse_quote!(i32),
1424            index: 0,
1425            is_public: true,
1426            kind: super::SlotKind::ScalarInt,
1427            prop_doc: None,
1428        };
1429        let slots = [&slot];
1430
1431        for cs in [ClassSystem::R6, ClassSystem::S7] {
1432            let out = generate_class_integration_r_code(cs, "T", &slots);
1433            assert_eq!(
1434                out.matches(".miniextendr_raise_condition(.val, sys.call())")
1435                    .count(),
1436                2,
1437                "{cs:?} integration getter+setter should each carry the condition guard:\n{out}"
1438            );
1439            assert!(
1440                !out.contains(".call = match.call()"),
1441                "{cs:?} integration code must not pass .call = match.call():\n{out}"
1442            );
1443        }
1444    }
1445}