miniextendr_macros/lib.rs
1//! # miniextendr-macros - Procedural macros for Rust <-> R interop
2//!
3//! This crate provides the procedural macros that power miniextendr's code
4//! generation. Most users should depend on `miniextendr-api` and use its
5//! re-exports, but this crate can be used directly when you only need macros.
6//!
7//! Primary macros and derives:
8//! - `#[miniextendr]` on functions, impl blocks, trait defs, and trait impls.
9//! - `#[r_ffi_checked]` for main-thread routing of C-ABI wrappers.
10//! - Derives: `ExternalPtr`, `RNativeType`, ALTREP derives, `RFactor`.
11//! - Helpers: `typed_list` for typed list builders.
12//!
13//! R wrapper generation is driven by Rust doc comments (roxygen tags are
14//! extracted). The `document` binary collects these wrappers and writes
15//! `R/miniextendr_wrappers.R` during package build.
16//!
17//! ## Quick start
18//!
19//! ```ignore
20//! use miniextendr_api::miniextendr;
21//!
22//! #[miniextendr]
23//! fn add(a: i32, b: i32) -> i32 {
24//! a + b
25//! }
26//! ```
27//!
28//! ## Macro expansion pipeline
29//!
30//! ### Overview
31//!
32//! ```text
33//! ┌──────────────────────────────────────────────────────────────────────────┐
34//! │ #[miniextendr] on fn │
35//! │ │
36//! │ 1. Parse: syn::ItemFn → MiniextendrFunctionParsed │
37//! │ 2. Analyze return type (Result<T>, Option<T>, raw SEXP, etc.) │
38//! │ 3. Generate: │
39//! │ ├── C wrapper: extern "C-unwind" fn C_<name>(call: SEXP, ...) → SEXP │
40//! │ ├── R wrapper: const R_WRAPPER_<NAME>: &str = "..." │
41//! │ └── Registration: const call_method_def_<name>: R_CallMethodDef │
42//! │ 4. Original function preserved (with added attributes) │
43//! └──────────────────────────────────────────────────────────────────────────┘
44//!
45//! ┌──────────────────────────────────────────────────────────────────────────┐
46//! │ #[miniextendr(env|r6|s3|s4|s7)] on impl │
47//! │ │
48//! │ 1. Parse: syn::ItemImpl → extract methods │
49//! │ 2. For each method: │
50//! │ ├── Generate C wrapper (handles self parameter) │
51//! │ ├── Generate R method wrapper string │
52//! │ └── Generate registration entry │
53//! │ 3. Generate class definition code per class system: │
54//! │ ├── env: new.env() + method assignment │
55//! │ ├── r6: R6Class() definition │
56//! │ ├── s3: S3 generics + methods │
57//! │ ├── s4: setClass() + setMethod() │
58//! │ └── s7: new_class() definition │
59//! │ 4. Emit const with combined R code │
60//! └──────────────────────────────────────────────────────────────────────────┘
61//!
62//! ┌──────────────────────────────────────────────────────────────────────────┐
63//! │ #[miniextendr] on trait │
64//! │ │
65//! │ 1. Parse: syn::ItemTrait → extract method signatures │
66//! │ 2. Generate: │
67//! │ ├── Trait tag constant: const TAG_<TRAIT>: mx_tag = ... │
68//! │ ├── Vtable struct: struct __vtable_<Trait> { ... } │
69//! │ └── CCalls table: static MX_CCALL_<TRAIT>: [...] = ... │
70//! │ 3. Original trait preserved │
71//! └──────────────────────────────────────────────────────────────────────────┘
72//!
73//! ┌──────────────────────────────────────────────────────────────────────────┐
74//! │ #[miniextendr] impl Trait for Type │
75//! │ │
76//! │ 1. Parse: syn::ItemImpl (trait impl) │
77//! │ 2. Generate: │
78//! │ ├── Vtable instance: static __VTABLE_<TRAIT>_FOR_<TYPE>: ... │
79//! │ ├── Wrapper struct: struct __MxWrapper<Type> { erased, data } │
80//! │ ├── Query function: fn __mx_query_<type>(tag) → vtable ptr │
81//! │ └── Base vtable: static __MX_BASE_VTABLE_<TYPE>: ... │
82//! │ 3. Original impl preserved │
83//! └──────────────────────────────────────────────────────────────────────────┘
84//!
85//! ```
86//!
87//! ### Key Modules
88//!
89//! | Module | Purpose |
90//! |--------|---------|
91//! | `miniextendr_fn` | Function parsing and attribute handling |
92//! | `c_wrapper_builder` | C wrapper generation (`extern "C-unwind"`) |
93//! | `r_wrapper_builder` | R wrapper code generation |
94//! | `rust_conversion_builder` | Rust→SEXP return value conversion |
95//! | `miniextendr_impl` | `impl Type` block processing |
96//! | `r_class_formatter` | Class system code generation (env/r6/s3/s4/s7) |
97//! | `miniextendr_trait` | Trait ABI metadata generation |
98//! | `miniextendr_impl_trait` | `impl Trait for Type` vtable generation |
99//! | `altrep` / `altrep_derive` | ALTREP struct derivation |
100//! | `externalptr_derive` | `#[derive(ExternalPtr)]` |
101//! | `roxygen` | Roxygen doc comment handling |
102//!
103//! ### Generated Symbol Naming
104//!
105//! For a function `my_func`:
106//! - C wrapper: `C_my_func`
107//! - R wrapper const: `R_WRAPPER_MY_FUNC`
108//! - Registration: `call_method_def_my_func`
109//!
110//! For a type `MyType` with trait `Counter`:
111//! - Vtable: `__VTABLE_COUNTER_FOR_MYTYPE`
112//! - Wrapper: `__MxWrapperMyType`
113//! - Query: `__mx_query_mytype`
114//!
115//! ## Return Type Handling
116//!
117//! The `return_type_analysis` module determines how to convert Rust returns to SEXP:
118//!
119//! | Rust Type | Strategy | R Result |
120//! |-----------|----------|----------|
121//! | `T: IntoR` | `.into_sexp()` | Converted value |
122//! | `Result<T, E>` | Unwrap or R error | Value or error |
123//! | `Option<T>` | `Some` → value, `None` → `NULL` | Value or NULL |
124//! | `SEXP` | Pass through | Raw SEXP |
125//! | `()` | Invisible NULL | `invisible(NULL)` |
126//!
127//! Use `#[miniextendr(unwrap_in_r)]` to return `Result<T, E>` to R without unwrapping.
128//!
129//! ## Thread Strategy
130//!
131//! By default, `#[miniextendr]` functions run on R's main thread. Opt into
132//! worker-thread execution with `#[miniextendr(worker)]` (requires the
133//! `worker-thread` feature on `miniextendr-api`). A worker opt-in is ignored
134//! when the signature requires main-thread execution (returns/takes `SEXP`,
135//! uses variadic dots, or sets `check_interrupt`).
136//!
137//! **Note**: `ExternalPtr<T>` is `Send` — it can be returned from worker
138//! thread functions. All R API operations on `ExternalPtr` are serialized
139//! through `with_r_thread`.
140//!
141//! ## Class Systems
142//!
143//! The `r_class_formatter` module generates R code for different class systems:
144//!
145//! | System | Generated R Code | Self Parameter |
146//! |--------|------------------|----------------|
147//! | `env` | `new.env()` with methods | `self` environment |
148//! | `r6` | `R6Class()` | `self` environment |
149//! | `s3` | `structure()` + generics | First argument |
150//! | `s4` | `setClass()` + `setMethod()` | First argument |
151//! | `s7` | `new_class()` | `self` property |
152
153// miniextendr-macros procedural macros
154
155mod altrep;
156mod c_wrapper_builder;
157mod list_macro;
158mod match_arg_keys;
159mod miniextendr_fn;
160mod type_inspect;
161mod typed_list;
162mod util;
163use crate::miniextendr_fn::{MiniextendrFnAttrs, MiniextendrFunctionParsed};
164mod miniextendr_impl;
165mod r_wrapper_builder;
166/// Builder utilities for formatting R wrapper arguments and calls.
167pub(crate) use r_wrapper_builder::RArgumentBuilder;
168mod rust_conversion_builder;
169/// Helper for generating Rust→R conversion code for return values.
170pub(crate) use rust_conversion_builder::RustConversionBuilder;
171mod method_return_builder;
172/// Helpers for shaping method return handling (R vs Rust wrapper code).
173pub(crate) use method_return_builder::{MethodReturnBuilder, ReturnStrategy};
174mod altrep_derive;
175mod dataframe_derive;
176mod lifecycle;
177mod list_derive;
178mod r_class_formatter;
179mod r_preconditions;
180mod return_type_analysis;
181mod roxygen;
182
183// Trait ABI support modules
184mod externalptr_derive;
185mod miniextendr_impl_trait;
186mod miniextendr_trait;
187mod typed_external_macro;
188
189// Factor support
190mod factor_derive;
191mod match_arg_derive;
192
193// Struct/enum dispatch for #[miniextendr] on structs and enums
194mod struct_enum_dispatch;
195
196// vctrs support
197#[cfg(feature = "vctrs")]
198mod vctrs_derive;
199mod vctrs_generics;
200
201mod naming;
202pub(crate) use naming::r_wrapper_const_ident_for;
203
204// Feature default mutual exclusivity guards
205#[cfg(all(feature = "default-r6", feature = "default-s7"))]
206compile_error!("`default-r6` and `default-s7` are mutually exclusive");
207// Note: default-main-thread was removed — main thread is now the hardcoded default.
208// default-worker still opts into worker thread execution.
209
210pub(crate) use type_inspect::{
211 SeveralOkContainer, classify_several_ok_container, first_type_argument, is_sexp_type,
212 second_type_argument,
213};
214pub(crate) use util::{extract_cfg_attrs, r_wrapper_raw_literal, source_location_doc};
215
216/// Validate the signature of an `extern "C-unwind"` fn exported via `#[miniextendr]`.
217///
218/// R's `.Call` interface passes all arguments as `SEXP` and expects a `SEXP`
219/// return value. For `extern "C-unwind"` functions the user writes the C symbol
220/// directly, so the signature must satisfy those invariants statically —
221/// otherwise the generated registration produces UB at runtime.
222///
223/// Called before any codegen so we fail fast on an invalid extern signature
224/// rather than emitting a wrapper that would only matter after the error.
225fn validate_extern_signature(
226 abi: &syn::Abi,
227 attrs: &[syn::Attribute],
228 inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]>,
229 output: &syn::ReturnType,
230) -> syn::Result<()> {
231 use syn::spanned::Spanned;
232
233 // Reject `self` receivers up front — they are never valid for `.Call`
234 // exports, and a missing-return-type error would only hide the real
235 // problem (users write `fn foo(self)` to ask "can I export a method?").
236 for input in inputs.iter() {
237 if let syn::FnArg::Receiver(recv) = input {
238 return Err(syn::Error::new_spanned(
239 recv,
240 "self parameter not allowed in standalone functions; \
241 use #[miniextendr(env|r6|s3|s4|s7)] on impl blocks instead",
242 ));
243 }
244 }
245
246 // Require one of #[no_mangle] / #[unsafe(no_mangle)] / #[export_name].
247 let has_no_mangle = attrs.iter().any(|attr| {
248 attr.path().is_ident("no_mangle")
249 || attr
250 .parse_nested_meta(|meta| {
251 if meta.path.is_ident("no_mangle") {
252 Err(meta.error("found #[no_mangle]"))
253 } else {
254 Ok(())
255 }
256 })
257 .is_err()
258 });
259 let has_export_name = attrs.iter().any(|attr| attr.path().is_ident("export_name"));
260 if !has_no_mangle && !has_export_name {
261 return Err(syn::Error::new(
262 attrs
263 .first()
264 .map(|attr| attr.span())
265 .unwrap_or_else(|| abi.span()),
266 "extern \"C-unwind\" functions need a visible C symbol for R's .Call interface. \
267 Add one of:\n \
268 - `#[unsafe(no_mangle)]` (Rust 2024 edition)\n \
269 - `#[no_mangle]` (Rust 2021 edition)\n \
270 - `#[export_name = \"my_symbol\"]` (custom symbol name)",
271 ));
272 }
273
274 // Return type must be SEXP.
275 match output {
276 non_return_type @ syn::ReturnType::Default => {
277 return Err(syn::Error::new(
278 non_return_type.span(),
279 "extern \"C-unwind\" functions used with #[miniextendr] must return SEXP. \
280 Add `-> miniextendr_api::ffi::SEXP` as the return type. \
281 If you want automatic type conversion, remove `extern \"C-unwind\"` and let \
282 the macro generate the C wrapper.",
283 ));
284 }
285 syn::ReturnType::Type(_rarrow, output_type) => match output_type.as_ref() {
286 syn::Type::Path(type_path) => {
287 if let Some(path_to_sexp) = type_path.path.segments.last().map(|x| &x.ident)
288 && path_to_sexp != "SEXP"
289 {
290 return Err(syn::Error::new(
291 path_to_sexp.span(),
292 format!(
293 "extern \"C-unwind\" functions must return SEXP, found `{path_to_sexp}`. \
294 R's .Call interface expects SEXP return values. \
295 Change the return type to `miniextendr_api::ffi::SEXP`, or remove \
296 `extern \"C-unwind\"` to let the macro handle type conversion.",
297 ),
298 ));
299 }
300 }
301 _ => {
302 return Err(syn::Error::new(
303 output_type.span(),
304 "extern \"C-unwind\" functions must return SEXP. \
305 R's .Call interface expects SEXP return values. \
306 Change the return type to `miniextendr_api::ffi::SEXP`, or remove \
307 `extern \"C-unwind\"` to let the macro handle type conversion.",
308 ));
309 }
310 },
311 }
312
313 // Every input must be SEXP. Reject variadics and receivers.
314 for input in inputs.iter() {
315 match input {
316 syn::FnArg::Receiver(recv) => {
317 return Err(syn::Error::new_spanned(
318 recv,
319 "extern \"C-unwind\" functions cannot have a `self` parameter. \
320 R's .Call interface only accepts SEXP arguments. \
321 Use `#[miniextendr(env|r6|s3|s4|s7)]` on an impl block for methods.",
322 ));
323 }
324 syn::FnArg::Typed(pat_type) => {
325 if let syn::Pat::Rest(_) = pat_type.pat.as_ref() {
326 return Err(syn::Error::new_spanned(
327 pat_type,
328 "extern functions cannot use variadic (...) - .Call passes fixed arguments",
329 ));
330 }
331 let is_sexp = match pat_type.ty.as_ref() {
332 syn::Type::Path(type_path) => type_path
333 .path
334 .segments
335 .last()
336 .is_some_and(|seg| seg.ident == "SEXP"),
337 _ => false,
338 };
339 if !is_sexp {
340 let is_dots_type = if let syn::Type::Reference(type_ref) = pat_type.ty.as_ref()
341 {
342 if let syn::Type::Path(inner) = type_ref.elem.as_ref() {
343 inner
344 .path
345 .segments
346 .last()
347 .is_some_and(|seg| seg.ident == "Dots")
348 } else {
349 false
350 }
351 } else if let syn::Type::Path(type_path) = pat_type.ty.as_ref() {
352 type_path
353 .path
354 .segments
355 .last()
356 .is_some_and(|seg| seg.ident == "Dots")
357 } else {
358 false
359 };
360 let msg = if is_dots_type {
361 "extern functions cannot use Dots; use `...` syntax in non-extern #[miniextendr] functions instead"
362 } else {
363 "extern function parameters must be SEXP - .Call passes all arguments as SEXP"
364 };
365 return Err(syn::Error::new_spanned(&pat_type.ty, msg));
366 }
367 }
368 }
369 }
370
371 Ok(())
372}
373
374/// Emit the `extern "C-unwind"` helper + `R_CallMethodDef` registration for
375/// each standalone-fn `match_arg` param.
376///
377/// Each helper returns the enum's `CHOICES` wrapped in a STRSXP so the R
378/// wrapper's prelude can call `match.arg(x, .Call(helper, ...))`. Factored
379/// out of the `miniextendr` fn body so the entry point doesn't own the
380/// `quote!` scaffolding.
381fn build_match_arg_helpers(
382 match_arg_param_info: &[(String, String, &syn::Type)],
383 parsed: &miniextendr_fn::MiniextendrFunctionParsed,
384 c_ident_str: &str,
385 cfg_attrs: &[syn::Attribute],
386) -> Vec<proc_macro2::TokenStream> {
387 match_arg_param_info
388 .iter()
389 .map(|(r_param, rust_name, param_ty)| {
390 // For several_ok, the param type is e.g. Vec<Mode>/Box<[Mode]>/[Mode; N]/&[Mode]
391 // — extract inner Mode for choices_sexp.
392 let choices_ty: &syn::Type = if parsed.has_several_ok(rust_name) {
393 classify_several_ok_container(param_ty)
394 .map(|(_, t)| t)
395 .unwrap_or(param_ty)
396 } else {
397 param_ty
398 };
399 let helper_fn_name = crate::match_arg_keys::choices_helper_c_name(c_ident_str, r_param);
400 let helper_fn_ident = syn::Ident::new(&helper_fn_name, proc_macro2::Span::call_site());
401 let helper_def_ident =
402 crate::match_arg_keys::choices_helper_def_ident(c_ident_str, r_param);
403 let helper_c_name = syn::LitCStr::new(
404 std::ffi::CString::new(helper_fn_name.clone())
405 .expect("valid C string")
406 .as_c_str(),
407 proc_macro2::Span::call_site(),
408 );
409 quote::quote! {
410 #(#cfg_attrs)*
411 #[allow(non_snake_case)]
412 #[unsafe(no_mangle)]
413 pub extern "C-unwind" fn #helper_fn_ident(
414 __miniextendr_call: ::miniextendr_api::ffi::SEXP,
415 ) -> ::miniextendr_api::ffi::SEXP {
416 ::miniextendr_api::choices_sexp::<#choices_ty>()
417 }
418
419 #(#cfg_attrs)*
420 #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_CALL_DEFS), linkme(crate = ::miniextendr_api::linkme))]
421 #[allow(non_upper_case_globals)]
422 #[allow(non_snake_case)]
423 static #helper_def_ident: ::miniextendr_api::ffi::R_CallMethodDef = unsafe {
424 ::miniextendr_api::ffi::R_CallMethodDef {
425 name: #helper_c_name.as_ptr(),
426 fun: Some(std::mem::transmute::<
427 unsafe extern "C-unwind" fn(
428 ::miniextendr_api::ffi::SEXP,
429 ) -> ::miniextendr_api::ffi::SEXP,
430 unsafe extern "C-unwind" fn() -> *mut ::std::os::raw::c_void,
431 >(#helper_fn_ident)),
432 numArgs: 1i32,
433 }
434 };
435 }
436 })
437 .collect()
438}
439
440/// Export Rust items to R.
441///
442/// `#[miniextendr]` can be applied to:
443/// - `fn` items (generate C + R wrappers)
444/// - `impl` blocks (generate R class methods)
445/// - `trait` items (generate trait ABI metadata)
446/// - ALTREP wrapper structs (generate `RegisterAltrep` impls)
447///
448/// # Functions
449///
450/// ```ignore
451/// use miniextendr_api::miniextendr;
452///
453/// #[miniextendr]
454/// fn add(a: i32, b: i32) -> i32 { a + b }
455/// ```
456///
457/// This produces a C wrapper `C_add` and an R wrapper `add()`.
458/// Registration is automatic via linkme distributed slices.
459///
460/// ## `extern "C-unwind"`
461///
462/// If the function is declared `extern "C-unwind"` and exported with
463/// `#[no_mangle]` (2021), `#[unsafe(no_mangle)]` (2024), or `#[export_name = "..."]`,
464/// the function itself is the C symbol and the R wrapper is prefixed with
465/// `unsafe_` to signal bypassed safety (no worker isolation or conversion).
466///
467/// ## Variadics (`...`)
468///
469/// Use `...` as the last argument. The Rust parameter becomes `_dots: &Dots`.
470/// Use `name @ ...` to give it a custom name (e.g., `args @ ...` → `args: &Dots`).
471///
472/// ### Typed Dots Validation
473///
474/// Use `#[miniextendr(dots = typed_list!(...))]` to automatically validate dots
475/// and create a `dots_typed` variable with typed accessors:
476///
477/// ```ignore
478/// #[miniextendr(dots = typed_list!(x => numeric(), y => integer(), z? => character()))]
479/// pub fn my_func(...) -> String {
480/// let x: f64 = dots_typed.get("x").expect("x");
481/// let y: i32 = dots_typed.get("y").expect("y");
482/// let z: Option<String> = dots_typed.get_opt("z").expect("z");
483/// format!("x={}, y={}", x, y)
484/// }
485/// ```
486///
487/// Type specs: `numeric()`, `integer()`, `logical()`, `character()`, `list()`,
488/// `raw()`, `complex()`, or `"class_name"` for class inheritance checks.
489/// Add `(n)` for exact length: `numeric(4)`. Use `?` suffix for optional fields.
490/// Use `@exact;` prefix for strict mode (reject extra fields).
491///
492/// ## Attributes
493///
494/// - `#[miniextendr(worker)]` — opt into worker-thread execution
495/// - `#[miniextendr(invisible)]` / `#[miniextendr(visible)]` — control return visibility
496/// - `#[miniextendr(check_interrupt)]` — check for user interrupt after call
497/// - `#[miniextendr(coerce)]` — coerce R type before conversion (also usable per-parameter)
498/// - `#[miniextendr(strict)]` — reject lossy conversions for i64/u64/isize/usize
499/// - `#[miniextendr(unwrap_in_r)]` — return `Result<T, E>` to R without unwrapping
500/// - `#[miniextendr(dots = typed_list!(...))]` — validate dots, create `dots_typed`
501/// - `#[miniextendr(internal)]` — adds `@keywords internal` to R wrapper
502/// - `#[miniextendr(noexport)]` — suppresses `@export` from R wrapper
503///
504/// # Impl blocks (class systems)
505///
506/// Apply `#[miniextendr(env|r6|s7|s3|s4)]` to an `impl Type` block.
507/// Use `#[miniextendr(label = "...")]` to disambiguate multiple impl blocks
508/// on the same type.
509/// Registration is automatic.
510///
511/// ## R6 Active Bindings
512///
513/// For R6 classes, use `#[miniextendr(r6(active))]` on methods to create
514/// active bindings (computed properties accessed without parentheses):
515///
516/// ```ignore
517/// use miniextendr_api::miniextendr;
518///
519/// pub struct Rectangle {
520/// width: f64,
521/// height: f64,
522/// }
523///
524/// #[miniextendr(r6)]
525/// impl Rectangle {
526/// pub fn new(width: f64, height: f64) -> Self {
527/// Self { width, height }
528/// }
529///
530/// /// Returns the area (width * height).
531/// #[miniextendr(r6(active))]
532/// pub fn area(&self) -> f64 {
533/// self.width * self.height
534/// }
535///
536/// /// Regular method (requires parentheses).
537/// pub fn scale(&mut self, factor: f64) {
538/// self.width *= factor;
539/// self.height *= factor;
540/// }
541/// }
542/// ```
543///
544/// In R:
545/// ```r
546/// r <- Rectangle$new(3, 4)
547/// r$area # 12 (active binding - no parentheses!)
548/// r$scale(2) # Regular method call
549/// r$area # 24
550/// ```
551///
552/// Active bindings must be getter-only methods taking only `&self`.
553///
554/// ## S7 Properties
555///
556/// For S7 classes, use `#[miniextendr(s7(getter))]` and `#[miniextendr(s7(setter))]`
557/// to create computed properties accessed via `@`:
558///
559/// ```ignore
560/// use miniextendr_api::{miniextendr, ExternalPtr};
561///
562/// #[derive(ExternalPtr)]
563/// pub struct Range {
564/// start: f64,
565/// end: f64,
566/// }
567///
568/// #[miniextendr(s7)]
569/// impl Range {
570/// pub fn new(start: f64, end: f64) -> Self {
571/// Self { start, end }
572/// }
573///
574/// /// Computed property (read-only): length of the range.
575/// #[miniextendr(s7(getter))]
576/// pub fn length(&self) -> f64 {
577/// self.end - self.start
578/// }
579///
580/// /// Dynamic property getter.
581/// #[miniextendr(s7(getter, prop = "midpoint"))]
582/// pub fn get_midpoint(&self) -> f64 {
583/// (self.start + self.end) / 2.0
584/// }
585///
586/// /// Dynamic property setter.
587/// #[miniextendr(s7(setter, prop = "midpoint"))]
588/// pub fn set_midpoint(&mut self, value: f64) {
589/// let half = self.length() / 2.0;
590/// self.start = value - half;
591/// self.end = value + half;
592/// }
593/// }
594/// ```
595///
596/// In R:
597/// ```r
598/// r <- Range(0, 10)
599/// r@length # 10 (computed, read-only)
600/// r@midpoint # 5 (dynamic property)
601/// r@midpoint <- 20 # Adjusts start/end to center at 20
602/// ```
603///
604/// ### Property Attributes
605///
606/// - `#[miniextendr(s7(getter))]` - Read-only computed property
607/// - `#[miniextendr(s7(getter, prop = "name"))]` - Named property getter
608/// - `#[miniextendr(s7(setter, prop = "name"))]` - Named property setter
609/// - `#[miniextendr(s7(getter, default = "0.0"))]` - Property with default value
610/// - `#[miniextendr(s7(getter, required))]` - Required property (error if not provided)
611/// - `#[miniextendr(s7(getter, frozen))]` - Property that can only be set once
612/// - `#[miniextendr(s7(getter, deprecated = "Use X instead"))]` - Deprecated property
613/// - `#[miniextendr(s7(validate))]` - Validator function for property
614///
615/// ## S7 Generic Dispatch Control
616///
617/// Control how S7 generics are created:
618///
619/// - `#[miniextendr(s7(no_dots))]` - Create strict generic without `...`
620/// - `#[miniextendr(s7(dispatch = "x,y"))]` - Multi-dispatch on multiple arguments
621/// - `#[miniextendr(s7(fallback))]` - Register method for `class_any` (catch-all).
622/// The generated R wrapper uses `tryCatch(x@.ptr, error = function(e) x)` to
623/// safely extract the self argument, so non-miniextendr objects won't crash with
624/// a slot-access error. Instead, incompatible objects produce a Rust type-conversion
625/// error when the method tries to interpret the argument as `&Self`.
626///
627/// ```ignore
628/// #[miniextendr(s7)]
629/// impl MyClass {
630/// /// Strict generic: function(x) instead of function(x, ...)
631/// #[miniextendr(s7(no_dots))]
632/// pub fn strict_method(&self) -> i32 { 42 }
633///
634/// /// Fallback method dispatched on class_any.
635/// /// Calling this on a non-MyClass object produces a type-conversion error,
636/// /// not a slot-access crash.
637/// #[miniextendr(s7(fallback))]
638/// pub fn describe(&self) -> String { "generic description".into() }
639/// }
640/// ```
641///
642/// ## S7 Type Conversion (`convert`)
643///
644/// Use `convert_from` and `convert_to` to enable S7's `convert()` for type coercion:
645///
646/// ```ignore
647/// use miniextendr_api::{miniextendr, ExternalPtr};
648///
649/// #[derive(ExternalPtr)]
650/// pub struct Celsius { value: f64 }
651///
652/// #[derive(ExternalPtr)]
653/// pub struct Fahrenheit { value: f64 }
654///
655/// #[miniextendr(s7)]
656/// impl Fahrenheit {
657/// pub fn new(value: f64) -> Self { Self { value } }
658///
659/// /// Convert FROM Celsius TO Fahrenheit.
660/// /// Usage: S7::convert(celsius_obj, Fahrenheit)
661/// #[miniextendr(s7(convert_from = "Celsius"))]
662/// pub fn from_celsius(c: ExternalPtr<Celsius>) -> Self {
663/// Fahrenheit { value: c.value * 9.0 / 5.0 + 32.0 }
664/// }
665///
666/// /// Convert FROM Fahrenheit TO Celsius.
667/// /// Usage: S7::convert(fahrenheit_obj, Celsius)
668/// #[miniextendr(s7(convert_to = "Celsius"))]
669/// pub fn to_celsius(&self) -> Celsius {
670/// Celsius { value: (self.value - 32.0) * 5.0 / 9.0 }
671/// }
672/// }
673/// ```
674///
675/// In R:
676/// ```r
677/// c <- Celsius(100)
678/// f <- S7::convert(c, Fahrenheit) # Uses convert_from
679/// c2 <- S7::convert(f, Celsius) # Uses convert_to
680/// ```
681///
682/// **Note:** Classes must be defined before they can be referenced in convert methods.
683/// Define the "from" class before the "to" class to avoid forward reference issues.
684///
685/// # Traits (ABI)
686///
687/// Apply `#[miniextendr]` to a trait to generate ABI metadata, then use
688/// `#[miniextendr] impl Trait for Type`. Registration is automatic.
689///
690/// # ALTREP
691///
692/// Apply `#[miniextendr(class = "...", base = "...")]` to a one-field
693/// wrapper struct. Registration is automatic.
694#[proc_macro_attribute]
695pub fn miniextendr(
696 attr: proc_macro::TokenStream,
697 item: proc_macro::TokenStream,
698) -> proc_macro::TokenStream {
699 // Try to parse as function first
700 if syn::parse::<syn::ItemFn>(item.clone()).is_ok() {
701 // Continue with function handling below
702 } else if syn::parse::<syn::ItemImpl>(item.clone()).is_ok() {
703 // Delegate to impl block parser
704 return miniextendr_impl::expand_impl(attr, item);
705 } else if syn::parse::<syn::ItemTrait>(item.clone()).is_ok() {
706 // Delegate to trait ABI generator
707 return miniextendr_trait::expand_trait(attr, item);
708 } else {
709 // Delegate to struct/enum dispatch (handles ALTREP, ExternalPtr, list, dataframe, factor, match_arg)
710 return struct_enum_dispatch::expand_struct_or_enum(attr, item);
711 }
712
713 let MiniextendrFnAttrs {
714 force_worker,
715 force_invisible,
716 check_interrupt,
717 coerce_all,
718 rng,
719 unwrap_in_r,
720 return_pref,
721 s3_generic,
722 s3_class,
723 dots_spec,
724 dots_span,
725 lifecycle,
726 strict,
727 internal,
728 noexport,
729 export,
730 doc,
731 error_in_r,
732 c_symbol,
733 r_name: fn_r_name,
734 r_entry,
735 r_post_checks,
736 r_on_exit,
737 } = syn::parse_macro_input!(attr as MiniextendrFnAttrs);
738
739 let mut parsed = syn::parse_macro_input!(item as MiniextendrFunctionParsed);
740
741 // Reject async functions
742 if let Some(asyncness) = &parsed.item().sig.asyncness {
743 return syn::Error::new_spanned(
744 asyncness,
745 "async functions are not supported by #[miniextendr]; \
746 R's C API is synchronous and incompatible with async executors",
747 )
748 .into_compile_error()
749 .into();
750 }
751
752 // Validate: reject generic functions (extern "C-unwind" + #[no_mangle] incompatible with generics).
753 // Lifetimes and type/const params are rejected for different reasons and get distinct messages.
754 {
755 let params = &parsed.item().sig.generics.params;
756 let has_lifetime = params
757 .iter()
758 .any(|p| matches!(p, syn::GenericParam::Lifetime(_)));
759 let has_type_or_const = params
760 .iter()
761 .any(|p| matches!(p, syn::GenericParam::Type(_) | syn::GenericParam::Const(_)));
762
763 if has_lifetime {
764 let err = syn::Error::new_spanned(
765 &parsed.item().sig.generics,
766 "#[miniextendr] functions cannot have explicit lifetime parameters. \
767 The generated `extern \"C-unwind\" #[no_mangle]` wrapper is incompatible \
768 with any generic parameter, including lifetimes. \
769 Use owned types (`Vec<T>` instead of `&[T]`, `String` instead of `&str`) \
770 or remove the explicit lifetime annotation — `&[T]` and `&str` arguments \
771 work without explicit lifetime params.",
772 );
773 return err.into_compile_error().into();
774 }
775 if has_type_or_const {
776 let err = syn::Error::new_spanned(
777 &parsed.item().sig.generics,
778 "#[miniextendr] functions cannot have generic type or const parameters. \
779 Generic functions are incompatible with `extern \"C-unwind\"` and `#[no_mangle]` \
780 required for R FFI. Consider using trait objects or monomorphization instead.",
781 );
782 return err.into_compile_error().into();
783 }
784 }
785
786 parsed.add_track_caller_if_needed();
787 parsed.add_inline_never_if_needed();
788
789 // Extract commonly used values
790 let uses_internal_c_wrapper = parsed.uses_internal_c_wrapper();
791 let c_ident = if let Some(ref sym) = c_symbol {
792 syn::Ident::new(sym, parsed.c_wrapper_ident().span())
793 } else {
794 parsed.c_wrapper_ident()
795 };
796 let r_wrapper_generator = parsed.r_wrapper_const_ident();
797
798 // Extract references to parsed components
799 let rust_ident = parsed.ident();
800 let inputs = parsed.inputs();
801 let output = parsed.output();
802 let abi = parsed.abi();
803 let attrs = parsed.attrs();
804 let vis = parsed.vis();
805 let generics = parsed.generics();
806 let has_dots = parsed.has_dots();
807 let named_dots = parsed.named_dots().cloned();
808
809 // Fail fast on invalid extern "C-unwind" signatures *before* any codegen,
810 // so we never emit a wrapper that would be discarded by the surfaced error.
811 if let Some(user_abi) = abi
812 && let Err(e) = validate_extern_signature(user_abi, attrs, inputs, output)
813 {
814 return e.into_compile_error().into();
815 }
816
817 // Check for @title/@description conflicts with implicit values (doc-lint feature)
818 // Skip when `doc` attribute overrides the roxygen — implicit docs are irrelevant then.
819 let doc_lint_warnings = if doc.is_some() {
820 proc_macro2::TokenStream::new()
821 } else {
822 crate::roxygen::doc_conflict_warnings(attrs, rust_ident.span())
823 };
824
825 // calling the rust function with
826 let rust_inputs: Vec<syn::Ident> = inputs
827 .iter()
828 .filter_map(|arg| {
829 if let syn::FnArg::Typed(pt) = arg
830 && let syn::Pat::Ident(p) = pt.pat.as_ref()
831 {
832 return Some(p.ident.clone());
833 }
834 None
835 })
836 .collect();
837 // dbg!(&rust_inputs);
838
839 // Validate dots_spec usage (actual injection happens later in the function body)
840 if dots_spec.is_some() && !has_dots {
841 let err = syn::Error::new(
842 dots_span.unwrap_or_else(proc_macro2::Span::call_site),
843 "#[miniextendr(dots = typed_list!(...))] requires a `...` parameter in the function signature",
844 );
845 return err.into_compile_error().into();
846 }
847
848 // Analyze return type to determine:
849 // - Whether it returns SEXP (affects thread strategy)
850 // - Whether result should be invisible
851 let rust_result_ident =
852 syn::Ident::new("__miniextendr_rust_result", proc_macro2::Span::mixed_site());
853 let return_analysis = return_type_analysis::analyze_return_type(
854 output,
855 &rust_result_ident,
856 rust_ident,
857 unwrap_in_r,
858 strict,
859 error_in_r,
860 );
861
862 let returns_sexp = return_analysis.returns_sexp;
863 let is_invisible_return_type = return_analysis.is_invisible;
864
865 // Apply explicit visibility override from #[miniextendr(invisible)] or #[miniextendr(visible)]
866 let is_invisible_return_type = force_invisible.unwrap_or(is_invisible_return_type);
867
868 // Check if any input parameter is SEXP (not Send, must stay on main thread)
869 let has_sexp_inputs = inputs.iter().any(|arg| {
870 if let syn::FnArg::Typed(pat_type) = arg {
871 is_sexp_type(pat_type.ty.as_ref())
872 } else {
873 false
874 }
875 });
876
877 // ═══════════════════════════════════════════════════════════════════════════
878 // Thread Strategy Selection
879 // ═══════════════════════════════════════════════════════════════════════════
880 //
881 // miniextendr supports two execution strategies:
882 //
883 // 1. **Main Thread Strategy** (with_r_unwind_protect) — DEFAULT
884 // - All code runs on R's main thread
885 // - Required when SEXP types are involved (not Send)
886 // - Required for R API calls (Rf_*, R_*)
887 // - Panic handling via R_UnwindProtect (Rust destructors run correctly)
888 // - Combined with error_in_r: errors returned as tagged SEXP values,
889 // R wrapper raises structured condition objects
890 // - Simpler execution model, better R integration
891 //
892 // 2. **Worker Thread Strategy** (run_on_worker + catch_unwind) — OPT-IN
893 // - Argument conversion on main thread (SEXP → Rust types)
894 // - Function execution on dedicated worker thread (clean panic isolation)
895 // - Result conversion on main thread (Rust types → SEXP)
896 // - Panic handling via catch_unwind (prevents unwinding across FFI boundary)
897 // - Opt in with #[miniextendr(worker)]
898 // - ExternalPtr<T> is Send: can be returned from worker thread functions
899 // - R API calls from worker use with_r_thread (serialized to main thread)
900 //
901 // Default: Main thread (safer with error_in_r, simpler execution model)
902 // Override: Use worker thread with #[miniextendr(worker)]
903 //
904 // Thread strategy:
905 // - Main thread is always used unless force_worker is set
906 // - force_worker cannot override hard requirements for main thread
907 // - Hard requirements: returns_sexp, has_sexp_inputs, has_dots, check_interrupt
908 let requires_main_thread = returns_sexp || has_sexp_inputs || has_dots || check_interrupt;
909 let use_main_thread = !force_worker || requires_main_thread;
910
911 // Extract cfg attributes to apply to generated items (needed by CWrapperContext)
912 let cfg_attrs = extract_cfg_attrs(parsed.attrs());
913
914 let source_loc_doc = source_location_doc(rust_ident.span());
915
916 // Build individual per-parameter coerce and match_arg_several_ok lists
917 let mut coerce_params_list: Vec<String> = Vec::new();
918 let mut match_arg_several_ok_params_list: Vec<String> = Vec::new();
919 for input in inputs.iter() {
920 if let syn::FnArg::Typed(pt) = input
921 && let syn::Pat::Ident(pat_ident) = pt.pat.as_ref()
922 {
923 let param_name = pat_ident.ident.to_string();
924 if parsed.has_coerce_attr(¶m_name) {
925 coerce_params_list.push(param_name.clone());
926 }
927 if parsed.has_match_arg_attr(¶m_name) && parsed.has_several_ok(¶m_name) {
928 match_arg_several_ok_params_list.push(param_name);
929 }
930 }
931 }
932
933 // Build the call expression: rust_ident(rust_input_1, rust_input_2, ...)
934 let fn_call_expr = quote::quote! { #rust_ident(#(#rust_inputs),*) };
935
936 // Determine return handling: use standalone-fn semantics (OptionIntoR for Option<T>)
937 // and handle unwrap_in_r (Result<T, E> → IntoR to pass result list to R).
938 let fn_return_handling = if unwrap_in_r && crate::return_type_analysis::output_is_result(output)
939 {
940 // Note: prefer= is intentionally ignored when unwrap_in_r overrides the return
941 // handling. In unwrap_in_r mode the IntoR here operates on the whole Result<T,E>
942 // (the framework's IntoR for Result encodes it as a tagged list for R to decode),
943 // NOT on the inner T. Applying prefer= would require Result<T,E>: IntoList /
944 // IntoExternalPtr / RNativeType — almost certainly absent. The prefer= attribute
945 // is effectively a no-op when unwrap_in_r is active.
946 c_wrapper_builder::ReturnHandling::IntoR
947 } else {
948 let auto = c_wrapper_builder::detect_return_handling_standalone_fn(output);
949 // Apply return_pref override: wraps the result in AsList/AsExternalPtr/AsRNative.
950 // Only applies to the plain IntoR variant — Option*/Result*/Unit/RawSexp/ExternalPtr
951 // variants pass through unchanged (see apply_return_pref docstring).
952 apply_return_pref(auto, return_pref)
953 };
954
955 let thread_strategy = if use_main_thread {
956 c_wrapper_builder::ThreadStrategy::MainThread
957 } else {
958 c_wrapper_builder::ThreadStrategy::WorkerThread
959 };
960
961 // Build CWrapperContext for the standalone fn
962 let mut c_wrapper_builder =
963 c_wrapper_builder::CWrapperContext::builder(rust_ident.clone(), c_ident.clone())
964 .r_wrapper_const(r_wrapper_generator.clone())
965 .inputs(inputs.iter().cloned().collect())
966 .output(output.clone())
967 .call_expr(fn_call_expr)
968 .thread_strategy(thread_strategy)
969 .return_handling(fn_return_handling)
970 .cfg_attrs(cfg_attrs.clone())
971 .vis(vis.clone())
972 .generics(generics.clone())
973 .preserve_param_names();
974
975 if uses_internal_c_wrapper {
976 // Normal Rust fn: generate full C wrapper
977 } else {
978 // extern "C-unwind" fn: user wrote the C symbol; only emit R_CallMethodDef
979 c_wrapper_builder = c_wrapper_builder.skip_wrapper();
980 }
981
982 if coerce_all {
983 c_wrapper_builder = c_wrapper_builder.coerce_all();
984 }
985 for param in coerce_params_list {
986 c_wrapper_builder = c_wrapper_builder.with_coerce_param(param);
987 }
988 for param in match_arg_several_ok_params_list {
989 c_wrapper_builder = c_wrapper_builder.match_arg_several_ok(param);
990 }
991 if check_interrupt {
992 c_wrapper_builder = c_wrapper_builder.check_interrupt();
993 }
994 if rng {
995 c_wrapper_builder = c_wrapper_builder.rng();
996 }
997 if strict {
998 c_wrapper_builder = c_wrapper_builder.strict();
999 }
1000 if error_in_r {
1001 c_wrapper_builder = c_wrapper_builder.error_in_r();
1002 }
1003
1004 let c_wrapper = c_wrapper_builder.build().generate();
1005
1006 // region: R wrappers generation in `fn`
1007 // Build R formal parameters and call arguments using shared builder
1008 let mut arg_builder = RArgumentBuilder::new(inputs);
1009 if has_dots {
1010 arg_builder = arg_builder.with_dots(named_dots.clone().map(|id| id.to_string()));
1011 }
1012 // Add user-specified parameter defaults (Missing<T> defaults handled via body prelude)
1013 let mut merged_defaults = parsed.param_defaults();
1014 // For match_arg params, always use the choices placeholder as the formal
1015 // default — the write-time pass replaces it with `c("a", "b", ...)`.
1016 // If the user supplied `default = "\"X\""`, capture X as `preferred_default`
1017 // so the write-time pass rotates X to position 1; the user's literal does
1018 // NOT become the formal (otherwise R's `match.arg` would only see one
1019 // choice).
1020 //
1021 // Tuple: (placeholder, rust_param, preferred_default_unquoted_or_empty)
1022 let mut match_arg_placeholders: Vec<(String, String, String)> = Vec::new();
1023 // (r_name, rust_param) pairs — used later to build @param doc placeholders
1024 let mut match_arg_r_names: Vec<(String, String)> = Vec::new();
1025 for match_arg_param in parsed.match_arg_params() {
1026 let r_name = r_wrapper_builder::normalize_r_arg_string(match_arg_param);
1027 match_arg_r_names.push((r_name.clone(), match_arg_param.clone()));
1028 let preferred = match merged_defaults.get(&r_name) {
1029 Some(raw) => crate::match_arg_keys::extract_match_arg_default(raw),
1030 None => String::new(),
1031 };
1032 let placeholder = crate::match_arg_keys::choices_placeholder(&c_ident.to_string(), &r_name);
1033 merged_defaults.insert(r_name.clone(), placeholder.clone());
1034 match_arg_placeholders.push((placeholder, match_arg_param.clone(), preferred));
1035 }
1036 // Add c("a", "b", "c") default for choices params (idiomatic R match.arg pattern)
1037 for (param_name, choices) in parsed.choices_params() {
1038 let r_name = r_wrapper_builder::normalize_r_arg_string(param_name);
1039 let quoted: Vec<String> = choices.iter().map(|c| format!("\"{}\"", c)).collect();
1040 merged_defaults
1041 .entry(r_name)
1042 .or_insert_with(|| format!("c({})", quoted.join(", ")));
1043 }
1044 arg_builder = arg_builder.with_defaults(merged_defaults);
1045
1046 let r_formals = arg_builder.build_formals();
1047 let mut r_call_args_strs = arg_builder.build_call_args_vec();
1048
1049 // Prepend .call parameter if using internal C wrapper
1050 if uses_internal_c_wrapper {
1051 r_call_args_strs.insert(0, ".call = match.call()".to_string());
1052 }
1053
1054 // Build the R body string consistently
1055 let c_ident_str = c_ident.to_string();
1056 let call_args_joined = r_call_args_strs.join(", ");
1057 let call_expr = if r_call_args_strs.is_empty() {
1058 format!(".Call({})", c_ident_str)
1059 } else {
1060 format!(".Call({}, {})", c_ident_str, call_args_joined)
1061 };
1062 let r_wrapper_return_str = if error_in_r {
1063 // error_in_r mode: capture result, check for error value, raise R condition
1064 let final_return = if is_invisible_return_type {
1065 "invisible(.val)"
1066 } else {
1067 ".val"
1068 };
1069 crate::method_return_builder::error_in_r_standalone_body(&call_expr, final_return, " ")
1070 } else if !is_invisible_return_type {
1071 call_expr
1072 } else {
1073 format!("invisible({})", call_expr)
1074 };
1075 // Determine R function name and S3-specific comments
1076 let is_s3_method = s3_generic.is_some() || s3_class.is_some();
1077 let r_wrapper_ident_str: String;
1078 let s3_method_comment: String;
1079
1080 if is_s3_method {
1081 // For S3 methods, function name is generic.class
1082 // generic defaults to Rust function name if not specified
1083 let generic = s3_generic.clone().unwrap_or_else(|| rust_ident.to_string());
1084 // s3_class is guaranteed to be Some here because MiniextendrFnAttrs::parse
1085 // validates that s3(...) always has class specified
1086 let class = s3_class.as_ref().expect("s3_class validated at parse time");
1087 r_wrapper_ident_str = format!("{}.{}", generic, class);
1088 // Add @importFrom for vctrs generics so roxygen registers the dependency
1089 let import_comment = if crate::vctrs_generics::is_vctrs_generic(&generic) {
1090 format!("#' @importFrom vctrs {}\n", generic)
1091 } else {
1092 String::new()
1093 };
1094 s3_method_comment = format!("{}#' @method {} {}\n", import_comment, generic, class);
1095 } else if let Some(ref custom_name) = fn_r_name {
1096 r_wrapper_ident_str = custom_name.clone();
1097 s3_method_comment = String::new();
1098 } else if abi.is_some() {
1099 r_wrapper_ident_str = format!("unsafe_{}", rust_ident);
1100 s3_method_comment = String::new();
1101 } else {
1102 r_wrapper_ident_str = rust_ident.to_string();
1103 s3_method_comment = String::new();
1104 };
1105
1106 // Stable, consistent R formatting style: brace on same line, body indented, closing brace on its own line
1107 // r_formals is already a joined string from build_formals()
1108 let formals_joined = r_formals;
1109 let mut roxygen_tags = if let Some(ref doc_text) = doc {
1110 // Custom doc override: each line becomes a separate roxygen tag entry
1111 doc_text.lines().map(|l| l.to_string()).collect()
1112 } else {
1113 crate::roxygen::roxygen_tags_from_attrs(attrs)
1114 };
1115
1116 // Determine lifecycle: explicit attr > #[deprecated] extraction
1117 let lifecycle_spec = lifecycle.or_else(|| {
1118 attrs
1119 .iter()
1120 .find_map(crate::lifecycle::parse_rust_deprecated)
1121 });
1122
1123 // Inject lifecycle badge into roxygen tags if present
1124 if let Some(ref spec) = lifecycle_spec {
1125 crate::lifecycle::inject_lifecycle_badge(&mut roxygen_tags, spec);
1126 }
1127
1128 // Auto-generate @param tags for every non-dots parameter the user didn't
1129 // already document. Priority, per param:
1130 // 1. choices(...) — quoted list, "One of ..." / "One or more of ..."
1131 // 2. match_arg — placeholder resolved at write time (#210)
1132 // 3. everything else — "(no documentation available)" fallback
1133 //
1134 // Collect (doc_placeholder, rust_param) as we go so the write-time resolver
1135 // registry gets an MX_MATCH_ARG_PARAM_DOCS entry for every match_arg param.
1136 let mut match_arg_param_doc_placeholders: Vec<(String, String)> = Vec::new();
1137 for arg in inputs.iter() {
1138 let syn::FnArg::Typed(pt) = arg else {
1139 continue;
1140 };
1141 let syn::Pat::Ident(pat_ident) = pt.pat.as_ref() else {
1142 continue;
1143 };
1144 if parsed.is_dots_param(&pat_ident.ident) {
1145 continue;
1146 }
1147 let rust_name = pat_ident.ident.to_string();
1148 let r_name = r_wrapper_builder::normalize_r_arg_ident(&pat_ident.ident).to_string();
1149 let already_documented = roxygen_tags
1150 .iter()
1151 .any(|t| t.trim_start().starts_with(&format!("@param {r_name}")));
1152 if already_documented {
1153 continue;
1154 }
1155
1156 if let Some(choices) = parsed.choices_for_param(&rust_name) {
1157 let quoted: Vec<String> = choices.iter().map(|c| format!("\"{}\"", c)).collect();
1158 let prefix = if parsed.has_several_ok(&rust_name) {
1159 "One or more of"
1160 } else {
1161 "One of"
1162 };
1163 roxygen_tags.push(format!("@param {r_name} {prefix} {}.", quoted.join(", ")));
1164 } else if parsed.has_match_arg_attr(&rust_name) {
1165 let doc_placeholder =
1166 crate::match_arg_keys::param_doc_placeholder(&c_ident.to_string(), &r_name);
1167 roxygen_tags.push(format!("@param {r_name} {doc_placeholder}"));
1168 match_arg_param_doc_placeholders.push((doc_placeholder, rust_name));
1169 } else {
1170 roxygen_tags.push(format!("@param {r_name} (no documentation available)"));
1171 }
1172 }
1173
1174 // Ensure a @title exists when we have auto-generated tags (e.g., @param from choices)
1175 // but the auto-title logic in roxygen_tags_from_attrs didn't fire (because has_any_tags
1176 // was false at that point — choices @param tags are added after extraction).
1177 // Prefer the implicit title from doc comments; fall back to the function name.
1178 if !roxygen_tags.is_empty() && !crate::roxygen::has_roxygen_tag(&roxygen_tags, "title") {
1179 let title = crate::roxygen::implicit_title_from_attrs(attrs)
1180 .unwrap_or_else(|| rust_ident.to_string().replace('_', " "));
1181 roxygen_tags.insert(0, format!("@title {}", title));
1182 }
1183
1184 let roxygen_tags_str = crate::roxygen::format_roxygen_tags(&roxygen_tags);
1185 let has_export_tag = crate::roxygen::has_roxygen_tag(&roxygen_tags, "export");
1186 let has_no_rd_tag = crate::roxygen::has_roxygen_tag(&roxygen_tags, "noRd");
1187 let has_internal_tag = crate::roxygen::has_roxygen_tag(&roxygen_tags, "keywords internal");
1188 // Add roxygen comments: @source for traceability, @export if public
1189 let source_comment = format!(
1190 "#' @source Generated by miniextendr from Rust fn `{}`\n",
1191 rust_ident
1192 );
1193 // Inject @keywords internal if #[miniextendr(internal)] and not already present
1194 let internal_comment = if internal && !has_internal_tag {
1195 "#' @keywords internal\n"
1196 } else {
1197 ""
1198 };
1199 // S3 methods need both @method (for registration) AND @export (for NAMESPACE)
1200 // Don't auto-export functions marked with @noRd, @keywords internal, or attr flags
1201 // #[miniextendr(export)] forces @export even on non-pub functions
1202 let export_comment = if (matches!(vis, syn::Visibility::Public(_)) || export)
1203 && !has_export_tag
1204 && !has_no_rd_tag
1205 && !has_internal_tag
1206 && !internal
1207 && !noexport
1208 {
1209 "#' @export\n".to_string()
1210 } else {
1211 String::new()
1212 };
1213 // Generate match.arg prelude for parameters with #[miniextendr(match_arg)]
1214 // Collect (r_param_name, rust_name, rust_type) for each match_arg param
1215 let match_arg_param_info: Vec<(String, String, &syn::Type)> = inputs
1216 .iter()
1217 .filter_map(|arg| {
1218 if let syn::FnArg::Typed(pt) = arg
1219 && let syn::Pat::Ident(pat_ident) = pt.pat.as_ref()
1220 {
1221 let rust_name = pat_ident.ident.to_string();
1222 if parsed.has_match_arg_attr(&rust_name) {
1223 let r_name =
1224 r_wrapper_builder::normalize_r_arg_ident(&pat_ident.ident).to_string();
1225 return Some((r_name, rust_name, pt.ty.as_ref()));
1226 }
1227 }
1228 None
1229 })
1230 .collect();
1231
1232 let match_arg_prelude = if match_arg_param_info.is_empty() {
1233 String::new()
1234 } else {
1235 let mut lines = Vec::new();
1236 for (r_param, rust_name, _) in &match_arg_param_info {
1237 // factor → character normalization
1238 lines.push(format!(
1239 "{param} <- if (is.factor({param})) as.character({param}) else {param}",
1240 param = r_param,
1241 ));
1242 // match.arg pulls the choice list off the formal default (which the
1243 // write-time pass has populated as `c("a", "b", ...)`), so no
1244 // explicit second arg is needed.
1245 if parsed.has_several_ok(rust_name) {
1246 lines.push(format!(
1247 "{param} <- base::match.arg({param}, several.ok = TRUE)",
1248 param = r_param,
1249 ));
1250 } else {
1251 lines.push(format!(
1252 "{param} <- base::match.arg({param})",
1253 param = r_param,
1254 ));
1255 }
1256 }
1257 lines.join("\n ")
1258 };
1259
1260 // Generate idiomatic match.arg prelude for choices params
1261 // These use the simpler pattern: `param <- match.arg(param)` (no C helper call needed)
1262 // With `several_ok`, emit `match.arg(param, several.ok = TRUE)` for multi-value selection
1263 let choices_prelude = {
1264 let mut lines = Vec::new();
1265 for arg in inputs.iter() {
1266 if let syn::FnArg::Typed(pt) = arg
1267 && let syn::Pat::Ident(pat_ident) = pt.pat.as_ref()
1268 {
1269 let rust_name = pat_ident.ident.to_string();
1270 if parsed.choices_for_param(&rust_name).is_some() {
1271 let r_name =
1272 r_wrapper_builder::normalize_r_arg_ident(&pat_ident.ident).to_string();
1273 if parsed.has_several_ok(&rust_name) {
1274 lines.push(format!(
1275 "{r_name} <- match.arg({r_name}, several.ok = TRUE)"
1276 ));
1277 } else {
1278 lines.push(format!("{r_name} <- match.arg({r_name})"));
1279 }
1280 }
1281 }
1282 }
1283 if lines.is_empty() {
1284 String::new()
1285 } else {
1286 lines.join("\n ")
1287 }
1288 };
1289
1290 // Generate lifecycle prelude if needed
1291 let lifecycle_prelude = lifecycle_spec
1292 .as_ref()
1293 .and_then(|spec| spec.r_prelude(&r_wrapper_ident_str));
1294
1295 // Generate R-side precondition checks (stopifnot + fallback precheck calls)
1296 // Skip both match_arg and choices params (already validated by match.arg)
1297 let mut skip_params: std::collections::HashSet<String> =
1298 parsed.match_arg_params().cloned().collect();
1299 for (param_name, _) in parsed.choices_params() {
1300 skip_params.insert(r_wrapper_builder::normalize_r_arg_string(param_name));
1301 }
1302 let precondition_output = r_preconditions::build_precondition_checks(inputs, &skip_params);
1303 let precondition_prelude = if precondition_output.static_checks.is_empty() {
1304 String::new()
1305 } else {
1306 precondition_output.static_checks.join("\n ")
1307 };
1308
1309 // Generate Missing<T> prelude: `if (missing(param)) param <- quote(expr=)`
1310 let missing_prelude = {
1311 let lines = r_wrapper_builder::build_missing_prelude(inputs, &parsed.param_defaults());
1312 if lines.is_empty() {
1313 String::new()
1314 } else {
1315 lines.join("\n ")
1316 }
1317 };
1318
1319 // Combine all preludes: r_entry, on.exit, missing defaults, lifecycle, static preconditions, match.arg, choices, r_post_checks
1320 let on_exit_str = r_on_exit.as_ref().map(|oe| oe.to_r_code());
1321 let combined_prelude = {
1322 let mut parts = Vec::new();
1323 if let Some(ref entry) = r_entry {
1324 parts.push(entry.as_str());
1325 }
1326 if let Some(ref s) = on_exit_str {
1327 parts.push(s.as_str());
1328 }
1329 if !missing_prelude.is_empty() {
1330 parts.push(missing_prelude.as_str());
1331 }
1332 if let Some(ref lc) = lifecycle_prelude {
1333 parts.push(lc.as_str());
1334 }
1335 if !precondition_prelude.is_empty() {
1336 parts.push(&precondition_prelude);
1337 }
1338 if !match_arg_prelude.is_empty() {
1339 parts.push(&match_arg_prelude);
1340 }
1341 if !choices_prelude.is_empty() {
1342 parts.push(&choices_prelude);
1343 }
1344 if let Some(ref post) = r_post_checks {
1345 parts.push(post.as_str());
1346 }
1347 if parts.is_empty() {
1348 None
1349 } else {
1350 Some(parts.join("\n "))
1351 }
1352 };
1353
1354 let r_wrapper_string = if let Some(prelude) = combined_prelude {
1355 format!(
1356 "{}{}{}{}{}{} <- function({}) {{\n {}\n {}\n}}",
1357 roxygen_tags_str,
1358 source_comment,
1359 s3_method_comment,
1360 internal_comment,
1361 export_comment,
1362 r_wrapper_ident_str,
1363 formals_joined,
1364 prelude,
1365 r_wrapper_return_str
1366 )
1367 } else {
1368 format!(
1369 "{}{}{}{}{}{} <- function({}) {{\n {}\n}}",
1370 roxygen_tags_str,
1371 source_comment,
1372 s3_method_comment,
1373 internal_comment,
1374 export_comment,
1375 r_wrapper_ident_str,
1376 formals_joined,
1377 r_wrapper_return_str
1378 )
1379 };
1380 // Use a raw string literal for better readability in macro expansion
1381 let r_wrapper_str = r_wrapper_raw_literal(&r_wrapper_string);
1382
1383 // endregion
1384
1385 // Generate doc strings with links
1386 let r_wrapper_doc = format!(
1387 "R wrapper code for [`{}`], calls [`{}`].",
1388 rust_ident, c_ident
1389 );
1390 let source_start = rust_ident.span().start();
1391 let source_line_lit = syn::LitInt::new(&source_start.line.to_string(), rust_ident.span());
1392 let source_col_lit =
1393 syn::LitInt::new(&(source_start.column + 1).to_string(), rust_ident.span());
1394
1395 // Get the normalized item for output, with roxygen tags stripped from docs.
1396 // Roxygen tags are for R documentation and shouldn't appear in rustdoc.
1397 let mut original_item = parsed.item_without_roxygen();
1398 // Strip only the miniextendr attributes; keep everything else.
1399 original_item
1400 .attrs
1401 .retain(|attr| !attr.path().is_ident("miniextendr"));
1402
1403 // Inject dots_typed binding into function body if dots = typed_list!(...) was specified
1404 if let Some(ref spec_tokens) = dots_spec {
1405 let dots_param = named_dots.clone().unwrap_or_else(|| {
1406 syn::Ident::new("__miniextendr_dots", proc_macro2::Span::call_site())
1407 });
1408 let validation_stmt: syn::Stmt = syn::parse_quote! {
1409 let dots_typed = #dots_param.typed(#spec_tokens)
1410 .expect("dots validation failed");
1411 };
1412 original_item.block.stmts.insert(0, validation_stmt);
1413 }
1414
1415 let original_item = original_item;
1416
1417 // Generate match_arg choices helper C wrappers and R_CallMethodDef entries
1418 let match_arg_helpers = build_match_arg_helpers(
1419 &match_arg_param_info,
1420 &parsed,
1421 &c_ident.to_string(),
1422 &cfg_attrs,
1423 );
1424
1425 // Generate MX_MATCH_ARG_CHOICES entries for placeholder → choices replacement
1426 // Resolve the `MatchArg`-bound type used in the choices_str closure: for
1427 // `several_ok` params that's the inner element of the container, otherwise
1428 // it's the param type directly.
1429 let choices_ty_for = |rust_param: &str| -> Option<&syn::Type> {
1430 let (_, _, param_ty) = match_arg_param_info
1431 .iter()
1432 .find(|(_, rn, _)| rn == rust_param)?;
1433 let ty: &syn::Type = if parsed.has_several_ok(rust_param) {
1434 classify_several_ok_container(param_ty)
1435 .map(|(_, t)| t)
1436 .unwrap_or(param_ty)
1437 } else {
1438 param_ty
1439 };
1440 Some(ty)
1441 };
1442
1443 let match_arg_choices_entries: Vec<proc_macro2::TokenStream> = match_arg_placeholders
1444 .iter()
1445 .filter_map(|(placeholder, rust_param, preferred_default)| {
1446 let choices_ty = choices_ty_for(rust_param)?;
1447 let entry_ident = syn::Ident::new(
1448 &format!(
1449 "match_arg_choices_entry_{}",
1450 crate::match_arg_keys::placeholder_ident_suffix(placeholder)
1451 ),
1452 proc_macro2::Span::call_site(),
1453 );
1454 Some(crate::match_arg_keys::choices_entry_tokens(
1455 &cfg_attrs,
1456 &entry_ident,
1457 placeholder,
1458 choices_ty,
1459 preferred_default,
1460 ))
1461 })
1462 .collect();
1463
1464 // Generate MX_MATCH_ARG_PARAM_DOCS entries for @param doc placeholder → choice description
1465 let match_arg_param_doc_entries: Vec<proc_macro2::TokenStream> =
1466 match_arg_param_doc_placeholders
1467 .iter()
1468 .filter_map(|(doc_placeholder, rust_param)| {
1469 let choices_ty = choices_ty_for(rust_param)?;
1470 let several_ok_lit = parsed.has_several_ok(rust_param);
1471 let entry_ident = syn::Ident::new(
1472 &format!(
1473 "match_arg_param_doc_entry_{}",
1474 crate::match_arg_keys::placeholder_ident_suffix(doc_placeholder)
1475 ),
1476 proc_macro2::Span::call_site(),
1477 );
1478 Some(crate::match_arg_keys::param_doc_entry_tokens(
1479 &cfg_attrs,
1480 &entry_ident,
1481 doc_placeholder,
1482 several_ok_lit,
1483 choices_ty,
1484 ))
1485 })
1486 .collect();
1487
1488 // Generate doc comment linking to C wrapper and R wrapper constant
1489 let fn_r_wrapper_doc = format!(
1490 "See [`{}`] for C wrapper, [`{}`] for R wrapper.",
1491 c_ident, r_wrapper_generator
1492 );
1493
1494 let expanded: proc_macro::TokenStream = quote::quote! {
1495 // rust function with doc link to R wrapper
1496 #[doc = #fn_r_wrapper_doc]
1497 #original_item
1498
1499 // C wrapper
1500 #(#cfg_attrs)*
1501 #c_wrapper
1502
1503 // R wrapper (self-registers via distributed_slice)
1504 #(#cfg_attrs)*
1505 #[doc = #r_wrapper_doc]
1506 #[doc = concat!("Wraps Rust function `", stringify!(#rust_ident), "`.")]
1507 #[doc = #source_loc_doc]
1508 #[doc = concat!("Generated from source file `", file!(), "`.")]
1509 #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_R_WRAPPERS), linkme(crate = ::miniextendr_api::linkme))]
1510 #[allow(non_upper_case_globals)]
1511 #[allow(non_snake_case)]
1512 static #r_wrapper_generator: ::miniextendr_api::registry::RWrapperEntry =
1513 ::miniextendr_api::registry::RWrapperEntry {
1514 priority: ::miniextendr_api::registry::RWrapperPriority::Function,
1515 source_file: file!(),
1516 content: concat!(
1517 "# Generated from Rust fn `",
1518 stringify!(#rust_ident),
1519 "` (",
1520 file!(),
1521 ":",
1522 #source_line_lit,
1523 ":",
1524 #source_col_lit,
1525 ")",
1526 #r_wrapper_str
1527 ),
1528 };
1529
1530 // match_arg choices helpers (C wrappers + R_CallMethodDef entries)
1531 // Each helper's call_method_def self-registers via distributed_slice
1532 #(#match_arg_helpers)*
1533
1534 // match_arg choices entries for R wrapper default replacement
1535 #(#match_arg_choices_entries)*
1536
1537 // match_arg @param doc entries for R wrapper roxygen doc replacement
1538 #(#match_arg_param_doc_entries)*
1539
1540 // doc-lint warnings (if any)
1541 #doc_lint_warnings
1542 }
1543 .into();
1544
1545 expanded
1546}
1547
1548/// Maps a `ReturnPref` attribute value onto an auto-detected `ReturnHandling`.
1549///
1550/// Only substitutes the plain `IntoR` variant with its pref-specific counterpart.
1551/// All other variants (`Unit`, `RawSexp`, `ExternalPtr`, `Result*`, `OptionIntoR`,
1552/// `OptionIntoRUnwrap`) are left unchanged — wrapping would be semantically wrong
1553/// or there is no plain value to wrap.
1554///
1555/// # Notes
1556///
1557/// When `prefer=` is combined with `Option<T>` or `Result<T,E>` returns, only the
1558/// plain-T fast path is affected — `Option`/`Result` wrappers behave as if `prefer=`
1559/// were absent. A future PR can add explicit `Option*` handling if a user needs it.
1560fn apply_return_pref(
1561 auto: c_wrapper_builder::ReturnHandling,
1562 pref: crate::miniextendr_fn::ReturnPref,
1563) -> c_wrapper_builder::ReturnHandling {
1564 use crate::miniextendr_fn::ReturnPref;
1565 use c_wrapper_builder::ReturnHandling;
1566
1567 match pref {
1568 ReturnPref::Auto => auto,
1569 ReturnPref::List => match auto {
1570 ReturnHandling::IntoR => ReturnHandling::AsListOf,
1571 other => other, // Unit, RawSexp, ExternalPtr, Result*, Option* — ignore prefer on incompatible types
1572 },
1573 ReturnPref::ExternalPtr => match auto {
1574 ReturnHandling::IntoR => ReturnHandling::AsExternalPtrOf,
1575 other => other,
1576 },
1577 ReturnPref::Native => match auto {
1578 ReturnHandling::IntoR => ReturnHandling::AsNativeOf,
1579 other => other,
1580 },
1581 }
1582}
1583
1584/// Generate thread-safe wrappers for R FFI functions.
1585///
1586/// Apply this to an `extern "C-unwind"` block to generate wrappers that ensure
1587/// R API calls happen on R's main thread.
1588///
1589/// # Behavior
1590///
1591/// All non-variadic functions are routed to the main thread via `with_r_thread`
1592/// when called from a worker thread. The return value is wrapped in `Sendable`
1593/// and sent back to the caller. This applies to both value-returning functions
1594/// (SEXP, i32, etc.) and pointer-returning functions (`*const T`, `*mut T`).
1595///
1596/// Pointer-returning functions (like `INTEGER`, `REAL`) are safe to route because
1597/// the underlying SEXP must be GC-protected by the caller, and R's GC only runs
1598/// during R API calls which are serialized through `with_r_thread`.
1599///
1600/// # Initialization Requirement
1601///
1602/// `miniextendr_runtime_init()` must be called before using any wrapped function.
1603/// Calling before initialization will panic with a descriptive error message.
1604///
1605/// # Limitations
1606///
1607/// - Variadic functions are passed through unchanged (no wrapper)
1608/// - Statics are passed through unchanged
1609/// - Functions with `#[link_name]` are passed through unchanged
1610///
1611/// # Example
1612///
1613/// ```ignore
1614/// #[r_ffi_checked]
1615/// unsafe extern "C-unwind" {
1616/// // Routed to main thread via with_r_thread when called from worker
1617/// pub fn Rf_ScalarInteger(arg1: i32) -> SEXP;
1618/// pub fn INTEGER(x: SEXP) -> *mut i32;
1619/// }
1620/// ```
1621#[proc_macro_attribute]
1622pub fn r_ffi_checked(
1623 _attr: proc_macro::TokenStream,
1624 item: proc_macro::TokenStream,
1625) -> proc_macro::TokenStream {
1626 let foreign_mod = syn::parse_macro_input!(item as syn::ItemForeignMod);
1627
1628 let foreign_mod_attrs = &foreign_mod.attrs;
1629 let abi = &foreign_mod.abi;
1630 let mut unchecked_items = Vec::new();
1631 let mut checked_wrappers = Vec::new();
1632
1633 for item in &foreign_mod.items {
1634 match item {
1635 syn::ForeignItem::Fn(fn_item) => {
1636 let is_variadic = fn_item.sig.variadic.is_some();
1637
1638 // Check if function already has #[link_name] - if so, pass through unchanged
1639 let has_link_name = fn_item
1640 .attrs
1641 .iter()
1642 .any(|attr| attr.path().is_ident("link_name"));
1643
1644 if is_variadic || has_link_name {
1645 // Pass through variadic functions and functions with explicit link_name unchanged
1646 unchecked_items.push(item.clone());
1647 } else {
1648 // Generate checked wrapper for non-variadic functions
1649 let vis = &fn_item.vis;
1650 let fn_name = &fn_item.sig.ident;
1651 let fn_name_str = fn_name.to_string();
1652 let unchecked_name = quote::format_ident!("{}_unchecked", fn_name);
1653 let unchecked_name_str = unchecked_name.to_string();
1654 let inputs = &fn_item.sig.inputs;
1655 let output = &fn_item.sig.output;
1656 // Filter out link_name attributes (already checked above, but be safe)
1657 let attrs: Vec<_> = fn_item
1658 .attrs
1659 .iter()
1660 .filter(|attr| !attr.path().is_ident("link_name"))
1661 .collect();
1662 let checked_doc = format!(
1663 "Checked wrapper for `{}`. Calls `{}` and routes through `with_r_thread`.",
1664 fn_name_str, unchecked_name_str
1665 );
1666 let checked_doc_lit = syn::LitStr::new(&checked_doc, fn_name.span());
1667 let source_loc_doc = crate::source_location_doc(fn_name.span());
1668 let source_loc_doc_lit = syn::LitStr::new(&source_loc_doc, fn_name.span());
1669
1670 // Generate the unchecked FFI binding with #[link_name]
1671 // Same visibility as the checked variant
1672 let link_name = syn::LitStr::new(&fn_name_str, fn_name.span());
1673 let unchecked_fn: syn::ForeignItem = syn::parse_quote! {
1674 #(#attrs)*
1675 #[doc = concat!("Unchecked FFI binding for `", stringify!(#fn_name), "`.")]
1676 #[doc = #source_loc_doc_lit]
1677 #[doc = concat!("Generated from source file `", file!(), "`.")]
1678 #[link_name = #link_name]
1679 #vis fn #unchecked_name(#inputs) #output;
1680 };
1681 unchecked_items.push(unchecked_fn);
1682
1683 // Generate a checked wrapper function
1684 let arg_names: Vec<_> = inputs
1685 .iter()
1686 .filter_map(|arg| {
1687 if let syn::FnArg::Typed(pat_type) = arg
1688 && let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref()
1689 {
1690 Some(pat_ident.ident.clone())
1691 } else {
1692 None
1693 }
1694 })
1695 .collect();
1696
1697 let is_never = matches!(output, syn::ReturnType::Type(_, ty) if matches!(**ty, syn::Type::Never(_)));
1698
1699 let wrapper = if is_never {
1700 // Never-returning functions (like Rf_error)
1701 quote::quote! {
1702 #(#attrs)*
1703 #[doc = #checked_doc_lit]
1704 #[doc = #source_loc_doc_lit]
1705 #[doc = concat!("Generated from source file `", file!(), "`.")]
1706 #[inline(always)]
1707 #[allow(non_snake_case)]
1708 #vis unsafe fn #fn_name(#inputs) #output {
1709 ::miniextendr_api::worker::with_r_thread(move || unsafe {
1710 #unchecked_name(#(#arg_names),*)
1711 })
1712 }
1713 }
1714 } else {
1715 // Normal functions - route via with_r_thread
1716 quote::quote! {
1717 #(#attrs)*
1718 #[doc = #checked_doc_lit]
1719 #[doc = #source_loc_doc_lit]
1720 #[doc = concat!("Generated from source file `", file!(), "`.")]
1721 #[inline(always)]
1722 #[allow(non_snake_case)]
1723 #vis unsafe fn #fn_name(#inputs) #output {
1724 let result = ::miniextendr_api::worker::with_r_thread(move || {
1725 ::miniextendr_api::worker::Sendable(unsafe {
1726 #unchecked_name(#(#arg_names),*)
1727 })
1728 });
1729 result.0
1730 }
1731 }
1732 };
1733 checked_wrappers.push(wrapper);
1734 }
1735 }
1736 _ => {
1737 // Pass through statics and other items unchanged
1738 unchecked_items.push(item.clone());
1739 }
1740 }
1741 }
1742
1743 let expanded = quote::quote! {
1744 #(#foreign_mod_attrs)*
1745 unsafe #abi {
1746 #(#unchecked_items)*
1747 }
1748
1749 #(#checked_wrappers)*
1750 };
1751
1752 expanded.into()
1753}
1754
1755/// Derive macro for implementing `RNativeType` on a newtype wrapper.
1756///
1757/// This allows newtype wrappers around R native types to work with `Vec<T>`,
1758/// `&[T]` conversions and the `Coerce<R>` traits.
1759/// The inner type must implement `RNativeType`.
1760///
1761/// # Supported Struct Forms
1762///
1763/// Both tuple structs and single-field named structs are supported:
1764///
1765/// ```ignore
1766/// use miniextendr_api::RNativeType;
1767///
1768/// // Tuple struct (most common)
1769/// #[derive(Clone, Copy, RNativeType)]
1770/// struct UserId(i32);
1771///
1772/// // Named single-field struct
1773/// #[derive(Clone, Copy, RNativeType)]
1774/// struct Temperature { celsius: f64 }
1775/// ```
1776///
1777/// # Generated Code
1778///
1779/// For `struct UserId(i32)`, this generates:
1780///
1781/// ```ignore
1782/// impl RNativeType for UserId {
1783/// const SEXP_TYPE: SEXPTYPE = <i32 as RNativeType>::SEXP_TYPE;
1784///
1785/// unsafe fn dataptr_mut(sexp: SEXP) -> *mut Self {
1786/// <i32 as RNativeType>::dataptr_mut(sexp).cast()
1787/// }
1788/// }
1789/// ```
1790///
1791/// # Using the Newtype with Coerce
1792///
1793/// Once `RNativeType` is derived, you can implement `Coerce` to/from the newtype:
1794///
1795/// ```ignore
1796/// impl Coerce<UserId> for i32 {
1797/// fn coerce(self) -> UserId { UserId(self) }
1798/// }
1799///
1800/// let id: UserId = 42.coerce();
1801/// ```
1802///
1803/// # Requirements
1804///
1805/// - Must be a newtype struct (exactly one field, tuple or named)
1806/// - The inner type must implement `RNativeType` (`i32`, `f64`, `RLogical`, `u8`, `Rcomplex`)
1807/// - Should also derive `Copy` (required by `RNativeType: Copy`)
1808#[proc_macro_derive(RNativeType)]
1809pub fn derive_rnative_type(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
1810 let input = syn::parse_macro_input!(input as syn::DeriveInput);
1811 let name = &input.ident;
1812 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1813
1814 // Extract inner type and constructor — must be a newtype (single field)
1815 let (inner_ty, elt_ctor): (syn::Type, proc_macro2::TokenStream) = match &input.data {
1816 syn::Data::Struct(data) => match &data.fields {
1817 syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
1818 let ty = fields.unnamed.first().unwrap().ty.clone();
1819 let ctor = quote::quote! { Self(val) };
1820 (ty, ctor)
1821 }
1822 syn::Fields::Named(fields) if fields.named.len() == 1 => {
1823 let field = fields.named.first().unwrap();
1824 let ty = field.ty.clone();
1825 let field_name = field.ident.as_ref().unwrap();
1826 let ctor = quote::quote! { Self { #field_name: val } };
1827 (ty, ctor)
1828 }
1829 _ => {
1830 return syn::Error::new_spanned(
1831 name,
1832 "#[derive(RNativeType)] requires a newtype struct with exactly one field",
1833 )
1834 .into_compile_error()
1835 .into();
1836 }
1837 },
1838 _ => {
1839 return syn::Error::new_spanned(name, "#[derive(RNativeType)] only works on structs")
1840 .into_compile_error()
1841 .into();
1842 }
1843 };
1844
1845 let expanded = quote::quote! {
1846 impl #impl_generics ::miniextendr_api::ffi::RNativeType for #name #ty_generics #where_clause {
1847 const SEXP_TYPE: ::miniextendr_api::ffi::SEXPTYPE =
1848 <#inner_ty as ::miniextendr_api::ffi::RNativeType>::SEXP_TYPE;
1849
1850 #[inline]
1851 unsafe fn dataptr_mut(sexp: ::miniextendr_api::ffi::SEXP) -> *mut Self {
1852 // Newtype is repr(transparent), so we can cast the pointer
1853 unsafe {
1854 <#inner_ty as ::miniextendr_api::ffi::RNativeType>::dataptr_mut(sexp).cast()
1855 }
1856 }
1857
1858 #[inline]
1859 fn elt(sexp: ::miniextendr_api::ffi::SEXP, i: isize) -> Self {
1860 let val = <#inner_ty as ::miniextendr_api::ffi::RNativeType>::elt(sexp, i);
1861 #elt_ctor
1862 }
1863 }
1864
1865 };
1866
1867 expanded.into()
1868}
1869
1870/// Derive macro for implementing `TypedExternal` on a type.
1871///
1872/// This makes the type compatible with `ExternalPtr<T>` for storing in R's external pointers.
1873///
1874/// # Basic Usage
1875///
1876/// ```ignore
1877/// use miniextendr_api::TypedExternal;
1878///
1879/// #[derive(ExternalPtr)]
1880/// struct MyData {
1881/// value: i32,
1882/// }
1883///
1884/// // Now you can use ExternalPtr<MyData>
1885/// let ptr = ExternalPtr::new(MyData { value: 42 });
1886/// ```
1887///
1888/// # Trait ABI
1889///
1890/// Trait dispatch wrappers are automatically generated:
1891///
1892/// ```ignore
1893/// use miniextendr_api::miniextendr;
1894///
1895/// #[derive(ExternalPtr)]
1896/// struct MyCounter {
1897/// value: i32,
1898/// }
1899///
1900/// #[miniextendr]
1901/// impl Counter for MyCounter {
1902/// fn value(&self) -> i32 { self.value }
1903/// fn increment(&mut self) { self.value += 1; }
1904/// }
1905/// ```
1906///
1907/// This generates additional infrastructure for type-erased trait dispatch:
1908/// - `__MxWrapperMyCounter` - Type-erased wrapper struct
1909/// - `__MX_BASE_VTABLE_MYCOUNTER` - Base vtable with drop/query
1910/// - `__mx_wrap_mycounter()` - Constructor returning `*mut mx_erased`
1911///
1912/// # Generated Code (Basic)
1913///
1914/// For a type `MyData` without traits:
1915///
1916/// ```ignore
1917/// impl TypedExternal for MyData {
1918/// const TYPE_NAME: &'static str = "MyData";
1919/// const TYPE_NAME_CSTR: &'static [u8] = b"MyData\0";
1920/// }
1921/// ```
1922#[proc_macro_derive(ExternalPtr, attributes(externalptr, r_data))]
1923pub fn derive_external_ptr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
1924 let input = syn::parse_macro_input!(input as syn::DeriveInput);
1925
1926 externalptr_derive::derive_external_ptr(input)
1927 .unwrap_or_else(|e| e.into_compile_error())
1928 .into()
1929}
1930
1931/// Derive macro for ALTREP integer vector data types.
1932///
1933/// Auto-implements `AltrepLen`, `AltIntegerData`, and calls `impl_altinteger_from_data!`.
1934///
1935/// # Attributes
1936///
1937/// - `#[altrep(len = "field_name")]` - Specify length field (auto-detects "len" or "length")
1938/// - `#[altrep(elt = "field_name")]` - For constant vectors, specify which field provides elements
1939/// - `#[altrep(dataptr)]` - Pass `dataptr` option to low-level macro
1940/// - `#[altrep(serialize)]` - Pass `serialize` option to low-level macro
1941/// - `#[altrep(subset)]` - Pass `subset` option to low-level macro
1942/// - `#[altrep(no_lowlevel)]` - Skip automatic `impl_altinteger_from_data!` call
1943///
1944/// # Example (Constant Vector - Zero Boilerplate!)
1945///
1946/// ```ignore
1947/// #[derive(ExternalPtr, AltrepInteger)]
1948/// #[altrep(elt = "value")] // All elements return this field
1949/// pub struct ConstantIntData {
1950/// value: i32,
1951/// len: usize,
1952/// }
1953///
1954/// // That's it! 3 lines instead of 30!
1955/// // AltrepLen, AltIntegerData, and low-level impls are auto-generated
1956///
1957/// #[miniextendr(class = "ConstantInt")]
1958/// pub struct ConstantIntClass(pub ConstantIntData);
1959/// ```
1960///
1961/// # Example (Custom elt() - Override One Method)
1962///
1963/// ```ignore
1964/// #[derive(ExternalPtr, AltrepInteger)]
1965/// pub struct ArithSeqData {
1966/// start: i32,
1967/// step: i32,
1968/// len: usize,
1969/// }
1970///
1971/// // Auto-generates AltrepLen and stub AltIntegerData
1972/// // Just override elt() for custom logic:
1973/// impl AltIntegerData for ArithSeqData {
1974/// fn elt(&self, i: usize) -> i32 {
1975/// self.start + (i as i32) * self.step
1976/// }
1977/// }
1978/// ```
1979#[proc_macro_derive(AltrepInteger, attributes(altrep))]
1980pub fn derive_altrep_integer(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
1981 let input = syn::parse_macro_input!(input as syn::DeriveInput);
1982 altrep_derive::derive_altrep_integer(input)
1983 .unwrap_or_else(|e| e.into_compile_error())
1984 .into()
1985}
1986
1987/// Derive macro for ALTREP real vector data types.
1988///
1989/// Auto-implements `AltrepLen` and `AltRealData` traits.
1990/// Supports the same `#[altrep(...)]` attributes as [`AltrepInteger`](derive@AltrepInteger).
1991#[proc_macro_derive(AltrepReal, attributes(altrep))]
1992pub fn derive_altrep_real(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
1993 let input = syn::parse_macro_input!(input as syn::DeriveInput);
1994 altrep_derive::derive_altrep_real(input)
1995 .unwrap_or_else(|e| e.into_compile_error())
1996 .into()
1997}
1998
1999/// Derive macro for ALTREP logical vector data types.
2000///
2001/// Auto-implements `AltrepLen` and `AltLogicalData` traits.
2002/// Supports the same `#[altrep(...)]` attributes as [`AltrepInteger`](derive@AltrepInteger).
2003#[proc_macro_derive(AltrepLogical, attributes(altrep))]
2004pub fn derive_altrep_logical(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2005 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2006 altrep_derive::derive_altrep_logical(input)
2007 .unwrap_or_else(|e| e.into_compile_error())
2008 .into()
2009}
2010
2011/// Derive macro for ALTREP raw vector data types.
2012///
2013/// Auto-implements `AltrepLen` and `AltRawData` traits.
2014/// Supports the same `#[altrep(...)]` attributes as [`AltrepInteger`](derive@AltrepInteger).
2015#[proc_macro_derive(AltrepRaw, attributes(altrep))]
2016pub fn derive_altrep_raw(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2017 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2018 altrep_derive::derive_altrep_raw(input)
2019 .unwrap_or_else(|e| e.into_compile_error())
2020 .into()
2021}
2022
2023/// Derive macro for ALTREP string vector data types.
2024///
2025/// Auto-implements `AltrepLen` and `AltStringData` traits.
2026/// Supports the same `#[altrep(...)]` attributes as [`AltrepInteger`](derive@AltrepInteger).
2027#[proc_macro_derive(AltrepString, attributes(altrep))]
2028pub fn derive_altrep_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2029 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2030 altrep_derive::derive_altrep_string(input)
2031 .unwrap_or_else(|e| e.into_compile_error())
2032 .into()
2033}
2034
2035/// Derive macro for ALTREP complex vector data types.
2036///
2037/// Auto-implements `AltrepLen` and `AltComplexData` traits.
2038/// Supports the same `#[altrep(...)]` attributes as [`AltrepInteger`](derive@AltrepInteger).
2039#[proc_macro_derive(AltrepComplex, attributes(altrep))]
2040pub fn derive_altrep_complex(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2041 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2042 altrep_derive::derive_altrep_complex(input)
2043 .unwrap_or_else(|e| e.into_compile_error())
2044 .into()
2045}
2046
2047/// Derive macro for ALTREP list vector data types.
2048///
2049/// Auto-implements `AltrepLen` and `AltListData` traits.
2050/// Supports the same `#[altrep(...)]` attributes as [`AltrepInteger`](derive@AltrepInteger),
2051/// except `dataptr` and `subset` which are not supported for list ALTREP.
2052#[proc_macro_derive(AltrepList, attributes(altrep))]
2053pub fn derive_altrep_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2054 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2055 altrep_derive::derive_altrep_list(input)
2056 .unwrap_or_else(|e| e.into_compile_error())
2057 .into()
2058}
2059
2060/// Derive ALTREP registration for a data struct.
2061///
2062/// Generates `TypedExternal`, `AltrepClass`, `RegisterAltrep`, `IntoR`,
2063/// linkme registration entry, and `Ref`/`Mut` accessor types.
2064///
2065/// The struct must already have low-level ALTREP traits implemented.
2066/// For most use cases, prefer a family-specific derive:
2067/// `#[derive(AltrepInteger)]`, `#[derive(AltrepReal)]`, etc.
2068/// Use `#[altrep(manual)]` on a family derive to skip data trait generation
2069/// when you provide your own `AltrepLen` + `Alt*Data` impls.
2070///
2071/// # Attributes
2072///
2073/// - `#[altrep(class = "Name")]` — custom ALTREP class name (defaults to struct name)
2074///
2075/// # Example
2076///
2077/// ```ignore
2078/// // Prefer family derives with manual:
2079/// #[derive(AltrepInteger)]
2080/// #[altrep(manual, class = "MyCustom", serialize)]
2081/// struct MyData { ... }
2082///
2083/// impl AltrepLen for MyData { ... }
2084/// impl AltIntegerData for MyData { ... }
2085/// ```
2086#[proc_macro_derive(Altrep, attributes(altrep))]
2087pub fn derive_altrep(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2088 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2089 altrep::derive_altrep(input)
2090 .unwrap_or_else(|e| e.into_compile_error())
2091 .into()
2092}
2093
2094/// Derive `IntoList` for a struct (Rust → R list).
2095///
2096/// - Named structs → named R list: `list(x = 1L, y = 2L)`
2097/// - Tuple structs → unnamed R list: `list(1L, 2L)`
2098/// - Fields annotated `#[into_list(ignore)]` are skipped
2099#[proc_macro_derive(IntoList, attributes(into_list))]
2100pub fn derive_into_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2101 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2102 list_derive::derive_into_list(input)
2103 .unwrap_or_else(|e| e.into_compile_error())
2104 .into()
2105}
2106
2107/// Derive `TryFromList` for a struct (R list → Rust).
2108///
2109/// - Named structs: extract by field name
2110/// - Tuple structs: extract by position (0, 1, 2, ...)
2111/// - Fields annotated `#[into_list(ignore)]` are not read and are initialized with `Default::default()`
2112#[proc_macro_derive(TryFromList, attributes(into_list))]
2113pub fn derive_try_from_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2114 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2115 list_derive::derive_try_from_list(input)
2116 .unwrap_or_else(|e| e.into_compile_error())
2117 .into()
2118}
2119
2120/// Derive `PrefersList`: when a type implements both `IntoList` and `ExternalPtr`,
2121/// this selects list as the default `IntoR` conversion.
2122///
2123/// Without a Prefer* derive, types that implement multiple conversion paths
2124/// will get a compile error due to conflicting `IntoR` impls.
2125///
2126/// # Example
2127///
2128/// ```ignore
2129/// #[derive(IntoList, PreferList)]
2130/// struct Config { verbose: bool, threads: i32 }
2131/// // IntoR produces list(verbose = TRUE, threads = 4L)
2132/// ```
2133#[proc_macro_derive(PreferList)]
2134pub fn derive_prefer_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2135 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2136 list_derive::derive_prefer_list(input)
2137 .unwrap_or_else(|e| e.into_compile_error())
2138 .into()
2139}
2140
2141/// Derive `PreferDataFrame`: when a type implements both `IntoDataFrame` (via `DataFrameRow`)
2142/// and other conversion paths, this selects data.frame as the default `IntoR` conversion.
2143///
2144/// # Example
2145///
2146/// ```ignore
2147/// #[derive(DataFrameRow, PreferDataFrame)]
2148/// struct Obs { time: f64, value: f64 }
2149/// // IntoR produces data.frame(time = ..., value = ...)
2150/// ```
2151#[proc_macro_derive(PreferDataFrame)]
2152pub fn derive_prefer_data_frame(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2153 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2154 list_derive::derive_prefer_data_frame(input)
2155 .unwrap_or_else(|e| e.into_compile_error())
2156 .into()
2157}
2158
2159/// Derive `PreferExternalPtr`: when a type implements both `ExternalPtr` and
2160/// other conversion paths (e.g., `IntoList`), this selects `ExternalPtr` wrapping
2161/// as the default `IntoR` conversion.
2162///
2163/// # Example
2164///
2165/// ```ignore
2166/// #[derive(ExternalPtr, IntoList, PreferExternalPtr)]
2167/// struct Model { weights: Vec<f64> }
2168/// // IntoR wraps as ExternalPtr (opaque R object), not list
2169/// ```
2170#[proc_macro_derive(PreferExternalPtr)]
2171pub fn derive_prefer_externalptr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2172 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2173 list_derive::derive_prefer_externalptr(input)
2174 .unwrap_or_else(|e| e.into_compile_error())
2175 .into()
2176}
2177
2178/// Derive `PreferRNativeType`: when a newtype wraps an `RNativeType` and also
2179/// implements other conversions, this selects the native R vector conversion
2180/// as the default `IntoR` path.
2181///
2182/// # Example
2183///
2184/// ```ignore
2185/// #[derive(Copy, Clone, RNativeType, PreferRNativeType)]
2186/// struct Meters(f64);
2187/// // IntoR produces a numeric scalar, not an ExternalPtr
2188/// ```
2189#[proc_macro_derive(PreferRNativeType)]
2190pub fn derive_prefer_rnative(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2191 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2192 list_derive::derive_prefer_rnative(input)
2193 .unwrap_or_else(|e| e.into_compile_error())
2194 .into()
2195}
2196
2197/// Derive `DataFrameRow`: generates a companion `*DataFrame` type with collection fields,
2198/// plus `IntoR` / `TryFromSexp` / `IntoDataFrame` impls for seamless R data.frame conversion.
2199///
2200/// # Example
2201///
2202/// ```ignore
2203/// #[derive(DataFrameRow)]
2204/// struct Measurement {
2205/// time: f64,
2206/// value: f64,
2207/// }
2208///
2209/// // Generates MeasurementDataFrame { time: Vec<f64>, value: Vec<f64> }
2210/// // plus conversion impls
2211/// ```
2212///
2213/// # Struct-level attributes
2214///
2215/// - `#[dataframe(name = "CustomDf")]` — custom name for the generated DataFrame type
2216/// - `#[dataframe(align)]` — pad shorter columns with NA to match longest
2217/// - `#[dataframe(tag = "my_tag")]` — attach a tag attribute to the data.frame
2218/// - `#[dataframe(conflicts = "string")]` — resolve conflicting column types as strings
2219///
2220/// # Field-level attributes
2221///
2222/// - `#[dataframe(skip)]` — omit this field from the DataFrame
2223/// - `#[dataframe(rename = "col")]` — custom column name
2224/// - `#[dataframe(as_list)]` — keep collection as single list column (no expansion)
2225/// - `#[dataframe(expand)]` / `#[dataframe(unnest)]` — expand collection into suffixed columns
2226/// - `#[dataframe(width = N)]` — pin expansion width (shorter rows get NA)
2227#[proc_macro_derive(DataFrameRow, attributes(dataframe))]
2228pub fn derive_dataframe_row(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2229 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2230 dataframe_derive::derive_dataframe_row(input)
2231 .unwrap_or_else(|e| e.into_compile_error())
2232 .into()
2233}
2234
2235/// Derive `RFactor`: enables conversion between Rust enums and R factors.
2236///
2237/// # Usage
2238///
2239/// ```ignore
2240/// #[derive(Copy, Clone, RFactor)]
2241/// enum Color {
2242/// Red,
2243/// Green,
2244/// Blue,
2245/// }
2246/// ```
2247///
2248/// # Attributes
2249///
2250/// - `#[r_factor(rename = "name")]` - Rename a variant's level string
2251/// - `#[r_factor(rename_all = "snake_case")]` - Rename all variants (snake_case, kebab-case, lower, upper)
2252#[proc_macro_derive(RFactor, attributes(r_factor))]
2253pub fn derive_r_factor(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2254 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2255 factor_derive::derive_r_factor(input)
2256 .unwrap_or_else(|e| e.into_compile_error())
2257 .into()
2258}
2259
2260/// Derive `MatchArg`: enables conversion between Rust enums and R character strings
2261/// with `match.arg` semantics (partial matching, informative errors).
2262///
2263/// # Usage
2264///
2265/// ```ignore
2266/// #[derive(Copy, Clone, MatchArg)]
2267/// enum Mode {
2268/// Fast,
2269/// Safe,
2270/// Debug,
2271/// }
2272/// ```
2273///
2274/// # Attributes
2275///
2276/// - `#[match_arg(rename = "name")]` - Rename a variant's choice string
2277/// - `#[match_arg(rename_all = "snake_case")]` - Rename all variants (snake_case, kebab-case, lower, upper)
2278///
2279/// # Generated Implementations
2280///
2281/// - `MatchArg` - Choice metadata and bidirectional conversion
2282/// - `TryFromSexp` - Convert R STRSXP/factor to enum (with partial matching)
2283/// - `IntoR` - Convert enum to R character scalar
2284#[proc_macro_derive(MatchArg, attributes(match_arg))]
2285pub fn derive_match_arg(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2286 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2287 match_arg_derive::derive_match_arg(input)
2288 .unwrap_or_else(|e| e.into_compile_error())
2289 .into()
2290}
2291
2292/// Derive `Vctrs`: enables creating vctrs-compatible S3 vector classes from Rust structs.
2293///
2294/// # Usage
2295///
2296/// ```ignore
2297/// #[derive(Vctrs)]
2298/// #[vctrs(class = "percent", base = "double")]
2299/// pub struct Percent {
2300/// data: Vec<f64>,
2301/// }
2302/// ```
2303///
2304/// # Attributes
2305///
2306/// - `#[vctrs(class = "name")]` - R class name (required)
2307/// - `#[vctrs(base = "type")]` - Base type: double, integer, logical, character, raw, list, record
2308/// - `#[vctrs(abbr = "abbr")]` - Abbreviation for `vec_ptype_abbr`
2309/// - `#[vctrs(inherit_base = true|false)]` - Whether to include base type in class vector
2310///
2311/// # Generated Implementations
2312///
2313/// - `VctrsClass` - Metadata trait for vctrs class information
2314/// - `VctrsRecord` (for `base = "record"`) - Field names for record types
2315#[cfg(feature = "vctrs")]
2316#[proc_macro_derive(Vctrs, attributes(vctrs))]
2317pub fn derive_vctrs(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2318 let input = syn::parse_macro_input!(input as syn::DeriveInput);
2319 vctrs_derive::derive_vctrs(input)
2320 .unwrap_or_else(|e| e.into_compile_error())
2321 .into()
2322}
2323
2324/// Create a `TypedListSpec` for validating `...` arguments or lists.
2325///
2326/// This macro provides ergonomic syntax for defining typed list specifications
2327/// that can be used with `Dots::typed()` to validate the structure of
2328/// `...` arguments passed from R.
2329///
2330/// # Syntax
2331///
2332/// ```text
2333/// typed_list!(
2334/// name => type_spec, // required field with type
2335/// name? => type_spec, // optional field with type
2336/// name, // required field, any type
2337/// name?, // optional field, any type
2338/// )
2339/// ```
2340///
2341/// For strict mode (no extra fields allowed):
2342/// ```text
2343/// typed_list!(@exact; name => type_spec, ...)
2344/// ```
2345///
2346/// # Type Specifications
2347///
2348/// ## Base types (with optional length)
2349/// - `numeric()` / `numeric(4)` - Real/double vector
2350/// - `integer()` / `integer(4)` - Integer vector
2351/// - `logical()` / `logical(4)` - Logical vector
2352/// - `character()` / `character(4)` - Character vector
2353/// - `raw()` / `raw(4)` - Raw vector
2354/// - `complex()` / `complex(4)` - Complex vector
2355/// - `list()` / `list(4)` - List (VECSXP)
2356///
2357/// ## Special types
2358/// - `data_frame()` - Data frame
2359/// - `factor()` - Factor
2360/// - `matrix()` - Matrix
2361/// - `array()` - Array
2362/// - `function()` - Function
2363/// - `environment()` - Environment
2364/// - `null()` - NULL only
2365/// - `any()` - Any type
2366///
2367/// ## String literals
2368/// - `"numeric"`, `"integer"`, etc. - Same as call syntax
2369/// - `"data.frame"` - Data frame (alias)
2370/// - `"MyClass"` - Any other string is treated as a class name (uses `Rf_inherits`)
2371///
2372/// # Examples
2373///
2374/// ## Basic usage
2375///
2376/// ```ignore
2377/// use miniextendr_api::{miniextendr, typed_list, Dots};
2378///
2379/// #[miniextendr]
2380/// pub fn process_args(dots: ...) -> Result<i32, String> {
2381/// let args = dots.typed(typed_list!(
2382/// alpha => numeric(4),
2383/// beta => list(),
2384/// gamma? => "character",
2385/// )).map_err(|e| e.to_string())?;
2386///
2387/// let alpha: Vec<f64> = args.get("alpha").map_err(|e| e.to_string())?;
2388/// Ok(alpha.len() as i32)
2389/// }
2390/// ```
2391///
2392/// ## Strict mode
2393///
2394/// ```ignore
2395/// // Reject any extra named fields
2396/// let args = dots.typed(typed_list!(@exact;
2397/// x => numeric(),
2398/// y => numeric(),
2399/// ))?;
2400/// ```
2401///
2402/// ## Class checking
2403///
2404/// ```ignore
2405/// // Check for specific R class (uses Rf_inherits semantics)
2406/// let args = dots.typed(typed_list!(
2407/// data => "data.frame",
2408/// model => "lm",
2409/// ))?;
2410/// ```
2411///
2412/// ## Attribute sugar
2413///
2414/// Instead of calling `.typed()` manually, you can use `typed_list!` directly in the
2415/// `#[miniextendr]` attribute for automatic validation:
2416///
2417/// ```ignore
2418/// #[miniextendr(dots = typed_list!(x => numeric(), y => numeric()))]
2419/// pub fn my_func(...) -> String {
2420/// // `dots_typed` is automatically created and validated
2421/// let x: f64 = dots_typed.get("x").expect("x");
2422/// let y: f64 = dots_typed.get("y").expect("y");
2423/// format!("x={}, y={}", x, y)
2424/// }
2425/// ```
2426///
2427/// This injects validation at the start of the function body:
2428/// ```ignore
2429/// let dots_typed = _dots.typed(typed_list!(...)).expect("dots validation failed");
2430/// ```
2431///
2432/// See the [`#[miniextendr]`](macro@miniextendr) attribute documentation for more details.
2433///
2434#[proc_macro]
2435pub fn typed_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2436 let parsed = syn::parse_macro_input!(input as typed_list::TypedListInput);
2437 typed_list::expand_typed_list(parsed).into()
2438}
2439
2440/// Construct an R list from Rust values.
2441///
2442/// This macro provides a convenient way to create R lists in Rust code,
2443/// using R-like syntax. Values are converted to R objects via the [`IntoR`] trait.
2444///
2445/// # Syntax
2446///
2447/// ```ignore
2448/// // Named entries (like R's list())
2449/// list!(
2450/// alpha = 1,
2451/// beta = "hello",
2452/// "my-name" = vec![1, 2, 3],
2453/// )
2454///
2455/// // Unnamed entries
2456/// list!(1, "hello", vec![1, 2, 3])
2457///
2458/// // Mixed (unnamed entries get empty string names)
2459/// list!(alpha = 1, 2, beta = "hello")
2460///
2461/// // Empty list
2462/// list!()
2463/// ```
2464///
2465/// # Examples
2466///
2467/// ```ignore
2468/// use miniextendr_api::{list, IntoR};
2469///
2470/// // Create a named list
2471/// let my_list = list!(
2472/// x = 42,
2473/// y = "hello world",
2474/// z = vec![1.0, 2.0, 3.0],
2475/// );
2476///
2477/// // In R this is equivalent to:
2478/// // list(x = 42L, y = "hello world", z = c(1, 2, 3))
2479/// ```
2480///
2481/// [`IntoR`]: https://docs.rs/miniextendr-api/latest/miniextendr_api/into_r/trait.IntoR.html
2482#[proc_macro]
2483pub fn list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2484 let parsed = syn::parse_macro_input!(input as list_macro::ListInput);
2485 list_macro::expand_list(parsed).into()
2486}
2487
2488/// Internal proc macro used by TPIE (Trait-Provided Impl Expansion).
2489///
2490/// Called by `__mx_impl_<Trait>!` macro_rules macros generated by `#[miniextendr]` on traits.
2491/// Do not call directly.
2492#[proc_macro]
2493#[doc(hidden)]
2494pub fn __mx_trait_impl_expand(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2495 miniextendr_impl_trait::expand_tpie(input)
2496}
2497
2498/// Generate `TypedExternal` and `IntoExternalPtr` impls for a concrete monomorphization
2499/// of a generic type.
2500///
2501/// Since `#[derive(ExternalPtr)]` rejects generic types, use this macro to generate
2502/// the necessary impls for a specific type instantiation.
2503///
2504/// # Example
2505///
2506/// ```ignore
2507/// struct Wrapper<T> { inner: T }
2508///
2509/// impl_typed_external!(Wrapper<i32>);
2510/// impl_typed_external!(Wrapper<String>);
2511/// ```
2512#[proc_macro]
2513pub fn impl_typed_external(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2514 match typed_external_macro::impl_typed_external(input.into()) {
2515 Ok(tokens) => tokens.into(),
2516 Err(err) => err.into_compile_error().into(),
2517 }
2518}
2519
2520/// Generate the `R_init_*` entry point for a miniextendr R package.
2521///
2522/// This macro consolidates all package initialization into a single line.
2523/// It generates an `extern "C-unwind"` function that R calls when loading
2524/// the shared library.
2525///
2526/// # Usage
2527///
2528/// ```ignore
2529/// // Auto-detects package name from CARGO_CRATE_NAME (recommended):
2530/// miniextendr_api::miniextendr_init!();
2531///
2532/// // Or specify explicitly (for edge cases):
2533/// miniextendr_api::miniextendr_init!(mypkg);
2534/// ```
2535///
2536/// The generated function calls `miniextendr_api::init::package_init` which
2537/// handles panic hooks, runtime init, locale assertion, ALTREP setup, trait ABI
2538/// registration, routine registration, and symbol locking.
2539#[proc_macro]
2540pub fn miniextendr_init(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2541 let pkg_name: syn::Ident = if input.is_empty() {
2542 // Auto-detect from CARGO_CRATE_NAME (set by cargo during compilation).
2543 // Cargo normalizes hyphens → underscores, so this is almost always a
2544 // valid Rust/C identifier. Still parse through syn so malformed values
2545 // surface as a compile error rather than an ICE.
2546 let name = match std::env::var("CARGO_CRATE_NAME") {
2547 Ok(n) => n,
2548 Err(_) => {
2549 return syn::Error::new(
2550 proc_macro2::Span::call_site(),
2551 "CARGO_CRATE_NAME not set. Either pass the package name explicitly: \
2552 miniextendr_init!(mypkg), or ensure you're building with cargo.",
2553 )
2554 .into_compile_error()
2555 .into();
2556 }
2557 };
2558 match syn::parse_str::<syn::Ident>(&name) {
2559 Ok(id) => id,
2560 Err(_) => {
2561 return syn::Error::new(
2562 proc_macro2::Span::call_site(),
2563 format!(
2564 "CARGO_CRATE_NAME `{name}` is not a valid C identifier; \
2565 R_init_<pkg> must match `[A-Za-z_][A-Za-z0-9_]*`. \
2566 Pass the name explicitly: miniextendr_init!(my_pkg)."
2567 ),
2568 )
2569 .into_compile_error()
2570 .into();
2571 }
2572 }
2573 } else {
2574 syn::parse_macro_input!(input as syn::Ident)
2575 };
2576 let fn_name = syn::Ident::new(&format!("R_init_{}", pkg_name), pkg_name.span());
2577 let unload_name = syn::Ident::new(&format!("R_unload_{}", pkg_name), pkg_name.span());
2578
2579 // Build a byte string literal with NUL terminator for the package name.
2580 let mut name_bytes = pkg_name.to_string().into_bytes();
2581 name_bytes.push(0);
2582 let byte_lit = syn::LitByteStr::new(&name_bytes, pkg_name.span());
2583
2584 let expanded = quote::quote! {
2585 // wasm32: pull in the host-generated `wasm_registry.rs` snapshot
2586 // (committed by the user crate, regenerated by `just wasm-prepare`).
2587 // The path is relative to the file invoking `miniextendr_init!()` —
2588 // by convention `<crate>/src/rust/lib.rs`, so the snapshot sits at
2589 // `<crate>/src/rust/wasm_registry.rs`. Module is `#[doc(hidden)]`
2590 // because it's purely an internal bridge between the user crate's
2591 // wrapper / vtable / register-fn `#[no_mangle]` exports and
2592 // `miniextendr_api::registry::install_wasm_runtime_slices`.
2593 #[cfg(target_arch = "wasm32")]
2594 #[path = "wasm_registry.rs"]
2595 #[doc(hidden)]
2596 mod __miniextendr_wasm_registry;
2597
2598 #[unsafe(no_mangle)]
2599 pub unsafe extern "C-unwind" fn #fn_name(
2600 dll: *mut ::miniextendr_api::ffi::DllInfo,
2601 ) {
2602 // wasm32: install the pre-generated runtime tables before
2603 // package_init runs. linkme didn't gather anything (the slices
2604 // are OnceLock-backed on wasm32), so register_routines /
2605 // universal_query would otherwise see empty slices.
2606 #[cfg(target_arch = "wasm32")]
2607 ::miniextendr_api::registry::install_wasm_runtime_slices(
2608 __miniextendr_wasm_registry::MX_CALL_DEFS_WASM,
2609 __miniextendr_wasm_registry::MX_ALTREP_REGISTRATIONS_WASM,
2610 __miniextendr_wasm_registry::MX_TRAIT_DISPATCH_WASM,
2611 );
2612
2613 unsafe {
2614 // SAFETY: byte literal is a valid NUL-terminated string produced by the macro.
2615 let pkg_name = ::std::ffi::CStr::from_bytes_with_nul_unchecked(#byte_lit);
2616 ::miniextendr_api::init::package_init(dll, pkg_name);
2617 }
2618 }
2619
2620 /// R_unload_<pkg> entry point — R calls this on `detach(unload=TRUE)` /
2621 /// `dyn.unload()`. Signals the miniextendr worker thread (if enabled)
2622 /// to exit cleanly. See `#103`.
2623 #[unsafe(no_mangle)]
2624 pub unsafe extern "C-unwind" fn #unload_name(
2625 _dll: *mut ::miniextendr_api::ffi::DllInfo,
2626 ) {
2627 ::miniextendr_api::worker::miniextendr_runtime_shutdown();
2628 }
2629
2630 /// Linker anchor: stub.c references this symbol to force the linker to pull
2631 /// in the user crate's archive member from the staticlib. With codegen-units = 1,
2632 /// this single member contains all linkme distributed_slice entries.
2633 /// The name is package-independent so stub.c doesn't need configure substitution.
2634 #[unsafe(no_mangle)]
2635 pub static miniextendr_force_link: ::std::ffi::c_char = 0;
2636 };
2637
2638 expanded.into()
2639}
2640
2641#[cfg(test)]
2642mod tests;