miniextendr_macros/rust_conversion_builder.rs
1//! Shared utilities for converting R SEXP parameters to Rust types.
2//!
3//! This module provides a builder for generating Rust conversion code from R SEXP arguments,
4//! ensuring consistent behavior across standalone functions and impl methods.
5
6use crate::miniextendr_fn::CoercionMapping;
7use proc_macro2::TokenStream;
8use quote::{quote, quote_spanned};
9use syn::spanned::Spanned;
10
11/// Builder for generating Rust conversion statements from R SEXP parameters.
12///
13/// Handles:
14/// - Unit types `()` → identity binding
15/// - `&Dots` → special wrapper with storage
16/// - Slices `&[T]` → TryFromSexp
17/// - `&str` → String + Borrow (for worker thread compatibility)
18/// - Scalar references → DATAPTR_RO_unchecked
19/// - Coercion → extract R native type + TryCoerce
20/// - Default → TryFromSexp
21pub struct RustConversionBuilder {
22 /// Enable coercion for all parameters
23 coerce_all: bool,
24 /// Parameter names that should use coercion
25 coerce_params: Vec<String>,
26 /// Enable strict input conversion for lossy types
27 strict: bool,
28 /// Parameter names with `match_arg + several_ok` — use `match_arg_vec_from_sexp` instead of `TryFromSexp`.
29 match_arg_several_ok_params: Vec<String>,
30}
31
32impl RustConversionBuilder {
33 /// Create a new conversion builder.
34 pub fn new() -> Self {
35 Self {
36 coerce_all: false,
37 coerce_params: Vec::new(),
38 strict: false,
39 match_arg_several_ok_params: Vec::new(),
40 }
41 }
42
43 /// Enable coercion for all parameters.
44 pub fn with_coerce_all(mut self) -> Self {
45 self.coerce_all = true;
46 self
47 }
48
49 /// Add a single parameter name that should use coercion.
50 ///
51 /// `param_name` is matched against the identifier in the function signature.
52 /// Can be called multiple times to add several parameters.
53 pub fn with_coerce_param(mut self, param_name: String) -> Self {
54 self.coerce_params.push(param_name);
55 self
56 }
57
58 /// Enable strict input conversion for lossy types (i64/u64/isize/usize + Vec variants).
59 pub fn with_strict(mut self) -> Self {
60 self.strict = true;
61 self
62 }
63
64 /// Mark a parameter as `match_arg + several_ok` — uses `match_arg_vec_from_sexp`
65 /// instead of `TryFromSexp` for converting STRSXP → `Vec<EnumType>`.
66 pub fn with_match_arg_several_ok(mut self, param_name: String) -> Self {
67 self.match_arg_several_ok_params.push(param_name);
68 self
69 }
70
71 /// Check if a parameter should use coercion.
72 ///
73 /// Returns `true` if `coerce_all` is set or `param_name` appears in the per-parameter list.
74 fn should_coerce(&self, param_name: &str) -> bool {
75 self.coerce_all || self.coerce_params.contains(¶m_name.to_string())
76 }
77
78 /// Generate a conversion expression that returns a tagged condition SEXP on failure.
79 ///
80 /// The R wrapper inspects `.val` and raises a structured `rust_*` condition; the
81 /// `return` happens from inside the C wrapper body before any further conversion.
82 ///
83 /// - `try_expr`: The `Result<T, E>`-producing expression
84 /// - `error_msg`: Human-readable error message for the failure
85 /// - `ident`: The binding name for the converted value
86 /// - `ty`: The target Rust type (for the `let` binding)
87 /// - `span`: Source span for error reporting
88 fn conversion_stmt(
89 &self,
90 try_expr: TokenStream,
91 error_msg: &str,
92 ident: &syn::Ident,
93 ty: &syn::Type,
94 span: proc_macro2::Span,
95 ) -> TokenStream {
96 quote_spanned! {span=>
97 let #ident: #ty = match #try_expr {
98 Ok(v) => v,
99 Err(e) => return ::miniextendr_api::error_value::make_rust_condition_value(
100 &format!("{}: {e}", #error_msg),
101 ::miniextendr_api::error_value::kind::CONVERSION,
102 ::core::option::Option::None,
103 Some(__miniextendr_call),
104 ),
105 };
106 }
107 }
108
109 /// Like `conversion_stmt` but without a type annotation on the binding.
110 fn conversion_stmt_untyped(
111 &self,
112 try_expr: TokenStream,
113 error_msg: &str,
114 ident: &syn::Ident,
115 span: proc_macro2::Span,
116 ) -> TokenStream {
117 quote_spanned! {span=>
118 let #ident = match #try_expr {
119 Ok(v) => v,
120 Err(e) => return ::miniextendr_api::error_value::make_rust_condition_value(
121 &format!("{}: {e}", #error_msg),
122 ::miniextendr_api::error_value::kind::CONVERSION,
123 ::core::option::Option::None,
124 Some(__miniextendr_call),
125 ),
126 };
127 }
128 }
129
130 /// Generate conversion statement for a single parameter.
131 ///
132 /// This is the non-split variant: owned conversions and borrow statements are
133 /// concatenated into a single list, suitable for main-thread execution where
134 /// everything runs in the same scope.
135 ///
136 /// - `pat_type`: the typed pattern from the function signature (e.g., `x: i32`).
137 /// - `sexp_ident`: the identifier of the raw SEXP variable holding the R argument.
138 ///
139 /// Returns a flat list of `let` binding statements that convert `sexp_ident` into
140 /// the Rust type declared in `pat_type`.
141 pub fn build_conversion(
142 &self,
143 pat_type: &syn::PatType,
144 sexp_ident: &syn::Ident,
145 ) -> Vec<TokenStream> {
146 // `build_conversion` is the single-scope (main-thread) path: every statement
147 // runs inside the same `with_r_unwind_protect` closure where the argument
148 // SEXPs are live for the whole call. So `&str` can borrow R's CHARSXP pool
149 // directly — zero-copy — exactly like `&[T]` already does. The owning-`String`
150 // detour only exists to satisfy `Send` when the value must cross the worker
151 // boundary (`build_conversion_split`), which never applies here.
152 let (owned, borrowed) = self.build_conversion_split_inner(pat_type, sexp_ident, true);
153 owned.into_iter().chain(borrowed).collect()
154 }
155
156 /// Generate conversion statements split into two phases for worker thread execution.
157 ///
158 /// For reference types like `&str`, we need to:
159 /// 1. Convert SEXP to owned type (String) -- runs on the main thread before the
160 /// worker closure, so the owned value can be moved into the closure.
161 /// 2. Borrow from the owned type (`&str`) -- runs inside the worker closure.
162 ///
163 /// For non-reference types (scalars, `Vec`, etc.) everything goes into the first
164 /// phase and the second vec is empty.
165 ///
166 /// - `pat_type`: the typed pattern from the function signature (e.g., `s: &str`).
167 /// - `sexp_ident`: the identifier of the raw SEXP variable holding the R argument.
168 ///
169 /// Returns `(owned_conversions, borrow_statements)` where each element is a list
170 /// of `let` binding token streams.
171 pub fn build_conversion_split(
172 &self,
173 pat_type: &syn::PatType,
174 sexp_ident: &syn::Ident,
175 ) -> (Vec<TokenStream>, Vec<TokenStream>) {
176 // Worker path: `&str` MUST be owned-then-borrowed because a borrowed view
177 // over R's CHARSXP pool is `!Send` and cannot move into the worker closure.
178 self.build_conversion_split_inner(pat_type, sexp_ident, false)
179 }
180
181 /// Inner implementation of [`build_conversion_split`].
182 ///
183 /// `zero_copy_str` controls how a `&str` argument is lowered:
184 /// - `true` (main-thread, single-scope): emit a direct zero-copy `&str` borrow
185 /// over R's CHARSXP pool via `TryFromSexp` — no `String` allocation. Sound
186 /// because the SEXP outlives the borrow inside the `with_r_unwind_protect`
187 /// closure, and the `&str`'s lifetime is tied to that scope (storing it beyond
188 /// the call is a borrow-checker error).
189 /// - `false` (worker): convert to an owned `String` on the main thread, then
190 /// borrow `&str` inside the worker closure — the `String` is `Send`, the
191 /// borrowed view is not.
192 fn build_conversion_split_inner(
193 &self,
194 pat_type: &syn::PatType,
195 sexp_ident: &syn::Ident,
196 zero_copy_str: bool,
197 ) -> (Vec<TokenStream>, Vec<TokenStream>) {
198 let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() else {
199 return (vec![], vec![]);
200 };
201 let ident = &pat_ident.ident;
202 let ty = pat_type.ty.as_ref();
203
204 match ty {
205 // Unit type: ()
206 // Note: We never generate `mut` on conversion bindings - the user's function
207 // has its own parameter binding that will be `mut` if they specified it.
208 syn::Type::Tuple(t) if t.elems.is_empty() => {
209 let stmt = quote! { let #ident = (); };
210 (vec![stmt], vec![])
211 }
212
213 // Reference types: &T, &mut T
214 syn::Type::Reference(r) => {
215 let param_name = ident.to_string();
216 let is_dots = matches!(
217 r.elem.as_ref(),
218 syn::Type::Path(tp)
219 if tp.path.segments.last()
220 .map(|s| s.ident == "Dots")
221 .unwrap_or(false)
222 );
223 let is_slice = matches!(r.elem.as_ref(), syn::Type::Slice(_));
224 let is_str = matches!(
225 r.elem.as_ref(),
226 syn::Type::Path(tp) if tp.path.is_ident("str")
227 );
228
229 // &[T] / &mut [T] with match_arg + several_ok:
230 // two-phase: pre-call Vec<T>, in-call borrow
231 if is_slice
232 && self
233 .match_arg_several_ok_params
234 .contains(¶m_name.to_string())
235 && let Some((crate::SeveralOkContainer::BorrowedSlice, inner_ty)) =
236 crate::classify_several_ok_container(ty)
237 {
238 let is_mut = r.mutability.is_some();
239 let storage_ident = quote::format_ident!("__storage_{}", ident);
240 let error_msg = format!(
241 "failed to convert parameter '{}' to &{}[{}]: invalid choice",
242 param_name,
243 if is_mut { "mut " } else { "" },
244 quote::quote!(#inner_ty)
245 );
246 let vec_ty: syn::Type = syn::parse_quote!(::std::vec::Vec<#inner_ty>);
247 let span = ty.span();
248 let try_expr = quote_spanned! {span=>
249 ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
250 };
251 // Emit owned Vec<T> binding.
252 // For &mut [T] the storage binding needs `mut`.
253 let owned_stmt = if is_mut {
254 // Need `let mut storage_ident: vec_ty = ...`; inline the mut variant.
255 let em = &error_msg;
256 quote_spanned! {span=>
257 let mut #storage_ident: #vec_ty = match #try_expr {
258 Ok(v) => v,
259 Err(e) => return ::miniextendr_api::error_value::make_rust_condition_value(
260 &format!("{}: {e}", #em),
261 ::miniextendr_api::error_value::kind::CONVERSION,
262 ::core::option::Option::None,
263 Some(__miniextendr_call),
264 ),
265 };
266 }
267 } else {
268 self.conversion_stmt(try_expr, &error_msg, &storage_ident, &vec_ty, span)
269 };
270 let borrow_stmt = if is_mut {
271 quote_spanned! {span=>
272 let #ident: #ty = &mut #storage_ident;
273 }
274 } else {
275 quote_spanned! {span=>
276 let #ident: #ty = &#storage_ident;
277 }
278 };
279 return (vec![owned_stmt], vec![borrow_stmt]);
280 }
281
282 if is_dots {
283 // &Dots: create wrapper with storage (main thread only - requires SEXP)
284 let storage_ident = quote::format_ident!("{}_storage", ident);
285 let stmt = quote! {
286 let #storage_ident = ::miniextendr_api::dots::Dots { inner: #sexp_ident };
287 let #ident = &#storage_ident;
288 };
289 (vec![stmt], vec![])
290 } else if is_slice {
291 // &[T]: use TryFromSexp (backed by DATAPTR_RO)
292 let error_msg = format!(
293 "failed to convert parameter '{}' to slice: wrong type or length",
294 ident
295 );
296 let span = ty.span();
297 let try_expr = quote_spanned! {span=>
298 ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident)
299 };
300 let stmt = self.conversion_stmt_untyped(try_expr, &error_msg, ident, span);
301 (vec![stmt], vec![])
302 } else if is_str {
303 let span = ty.span();
304 let error_msg = format!(
305 "failed to convert parameter '{}' to string: expected character vector",
306 ident
307 );
308 if zero_copy_str {
309 // Main-thread path: borrow R's CHARSXP pool directly via the
310 // `&'static str` TryFromSexp impl — zero allocation. The SEXP
311 // is a live wrapper argument for the whole call, so the borrow
312 // is sound; its lifetime is tied to this scope, so storing it
313 // beyond the call is a borrow-checker error (no-store guarantee).
314 let try_expr = quote_spanned! {span=>
315 ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident)
316 };
317 let stmt = self.conversion_stmt_untyped(try_expr, &error_msg, ident, span);
318 (vec![stmt], vec![])
319 } else {
320 // Worker path: convert to owned String, then borrow using the
321 // Borrow trait. The String moves into the worker closure (it is
322 // Send); a borrowed view over R's CHARSXP pool is not.
323 let owned_ident = quote::format_ident!("__owned_{}", ident);
324 // Owned conversion: SEXP -> String
325 let string_ty: syn::Type = syn::parse_quote!(String);
326 let try_expr = quote_spanned! {span=>
327 ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident)
328 };
329 let owned_stmt = self.conversion_stmt(
330 try_expr,
331 &error_msg,
332 &owned_ident,
333 &string_ty,
334 span,
335 );
336 // Borrow: String -> &str (using Borrow trait)
337 let borrow_stmt = quote_spanned! {span=>
338 let #ident: &str = ::std::borrow::Borrow::borrow(&#owned_ident);
339 };
340 (vec![owned_stmt], vec![borrow_stmt])
341 }
342 } else {
343 // &T for other types: use TryFromSexp for the reference type.
344 let error_msg = format!(
345 "failed to convert parameter '{}' to {}: wrong type",
346 ident,
347 quote!(#ty)
348 );
349 let span = ty.span();
350 let try_expr = quote_spanned! {span=>
351 ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident)
352 };
353 let stmt = self.conversion_stmt(try_expr, &error_msg, ident, ty, span);
354 (vec![stmt], vec![])
355 }
356 }
357
358 // All other types
359 _ => {
360 let param_name = ident.to_string();
361
362 // Strict mode: use checked input helpers for lossy types
363 if self.strict
364 && let Some(strict_expr) =
365 crate::return_type_analysis::strict_input_conversion_for_type(
366 ty,
367 sexp_ident,
368 ¶m_name,
369 )
370 {
371 let span = ty.span();
372 let stmt = quote_spanned! {span=>
373 let #ident: #ty = #strict_expr;
374 };
375 return (vec![stmt], vec![]);
376 }
377
378 // match_arg + several_ok: use match_arg_vec_from_sexp for container types
379 if self
380 .match_arg_several_ok_params
381 .contains(¶m_name.to_string())
382 && let Some((container, inner_ty)) = crate::classify_several_ok_container(ty)
383 {
384 let span = ty.span();
385 match container {
386 crate::SeveralOkContainer::Vec => {
387 let error_msg = format!(
388 "failed to convert parameter '{}' to Vec<{}>: invalid choice",
389 param_name,
390 quote!(#inner_ty)
391 );
392 let try_expr = quote_spanned! {span=>
393 ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
394 };
395 let stmt = self.conversion_stmt(try_expr, &error_msg, ident, ty, span);
396 return (vec![stmt], vec![]);
397 }
398 crate::SeveralOkContainer::BoxedSlice => {
399 let error_msg = format!(
400 "failed to convert parameter '{}' to Box<[{}]>: invalid choice",
401 param_name,
402 quote!(#inner_ty)
403 );
404 let try_expr = quote_spanned! {span=>
405 ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
406 .map(|v| v.into_boxed_slice())
407 };
408 let stmt = self.conversion_stmt(try_expr, &error_msg, ident, ty, span);
409 return (vec![stmt], vec![]);
410 }
411 crate::SeveralOkContainer::Array(n) => {
412 let error_msg = format!(
413 "failed to convert parameter '{}': invalid choice",
414 param_name,
415 );
416 let param_name_lit = ¶m_name;
417 let span = ty.span();
418 // First extract the Vec via match_arg_vec_from_sexp (handles
419 // match_arg validation + error reporting), then convert length-check
420 // separately via a direct panic (caught by the framework).
421 let vec_ty: syn::Type = syn::parse_quote!(::std::vec::Vec<#inner_ty>);
422 let vec_ident = quote::format_ident!("__vec_{}", ident);
423 let try_expr = quote_spanned! {span=>
424 ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
425 };
426 let vec_stmt = self
427 .conversion_stmt(try_expr, &error_msg, &vec_ident, &vec_ty, span);
428 // Length check + array conversion via panic (framework catches panics)
429 let arr_stmt = quote_spanned! {span=>
430 let #ident: #ty = {
431 if #vec_ident.len() != #n {
432 panic!(
433 "parameter `{}`: expected {} values for [_; {}], got {}",
434 #param_name_lit, #n, #n, #vec_ident.len()
435 );
436 }
437 <[#inner_ty; #n]>::try_from(#vec_ident)
438 .unwrap_or_else(|_| unreachable!())
439 };
440 };
441 return (vec![vec_stmt, arr_stmt], vec![]);
442 }
443 crate::SeveralOkContainer::BorrowedSlice => {
444 let storage_ident = quote::format_ident!("__storage_{}", ident);
445 let error_msg = format!(
446 "failed to convert parameter '{}' to &[{}]: invalid choice",
447 param_name,
448 quote!(#inner_ty)
449 );
450 let vec_ty: syn::Type = syn::parse_quote!(::std::vec::Vec<#inner_ty>);
451 let try_expr = quote_spanned! {span=>
452 ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
453 };
454 let owned_stmt = self.conversion_stmt(
455 try_expr,
456 &error_msg,
457 &storage_ident,
458 &vec_ty,
459 span,
460 );
461 let borrow_stmt = quote_spanned! {span=>
462 let #ident: #ty = &#storage_ident;
463 };
464 return (vec![owned_stmt], vec![borrow_stmt]);
465 }
466 }
467 }
468
469 let should_coerce = self.should_coerce(¶m_name);
470 let coercion_mapping = if should_coerce {
471 CoercionMapping::from_type(ty)
472 } else {
473 None
474 };
475
476 let span = ty.span();
477 let stmt = match coercion_mapping {
478 Some(CoercionMapping::Scalar { r_native, target }) => {
479 let error_msg_convert = format!(
480 "failed to convert parameter '{}' from R: wrong type",
481 param_name
482 );
483 let error_msg_coerce = format!(
484 "failed to coerce parameter '{}' to {}: overflow, NaN, or precision loss",
485 param_name,
486 quote!(#target)
487 );
488 quote_spanned! {span=>
489 let #ident: #target = {
490 let __r_val: #r_native = match ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident) {
491 Ok(v) => v,
492 Err(e) => return ::miniextendr_api::error_value::make_rust_condition_value(
493 &format!("{}: {e}", #error_msg_convert),
494 ::miniextendr_api::error_value::kind::CONVERSION,
495 ::core::option::Option::None,
496 Some(__miniextendr_call),
497 ),
498 };
499 match ::miniextendr_api::TryCoerce::<#target>::try_coerce(__r_val) {
500 Ok(v) => v,
501 Err(e) => return ::miniextendr_api::error_value::make_rust_condition_value(
502 &format!("{}: {e}", #error_msg_coerce),
503 ::miniextendr_api::error_value::kind::CONVERSION,
504 ::core::option::Option::None,
505 Some(__miniextendr_call),
506 ),
507 }
508 };
509 }
510 }
511 Some(CoercionMapping::Vec {
512 r_native_elem,
513 target_elem,
514 }) => {
515 let error_msg_convert = format!(
516 "failed to convert parameter '{}' to vector: wrong type",
517 param_name
518 );
519 let error_msg_coerce = format!(
520 "failed to coerce parameter '{}' to Vec<{}>: element overflow, NaN, or precision loss",
521 param_name,
522 quote!(#target_elem)
523 );
524 quote_spanned! {span=>
525 let #ident: Vec<#target_elem> = {
526 let __r_slice: &[#r_native_elem] = match ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident) {
527 Ok(v) => v,
528 Err(e) => return ::miniextendr_api::error_value::make_rust_condition_value(
529 &format!("{}: {e}", #error_msg_convert),
530 ::miniextendr_api::error_value::kind::CONVERSION,
531 ::core::option::Option::None,
532 Some(__miniextendr_call),
533 ),
534 };
535 match __r_slice.iter().copied()
536 .map(::miniextendr_api::TryCoerce::<#target_elem>::try_coerce)
537 .collect::<Result<Vec<_>, _>>()
538 {
539 Ok(v) => v,
540 Err(e) => return ::miniextendr_api::error_value::make_rust_condition_value(
541 &format!("{}: {e}", #error_msg_coerce),
542 ::miniextendr_api::error_value::kind::CONVERSION,
543 ::core::option::Option::None,
544 Some(__miniextendr_call),
545 ),
546 }
547 };
548 }
549 }
550 None => {
551 let error_msg = format!(
552 "failed to convert parameter '{}' to {}: wrong type, length, or contains NA",
553 param_name,
554 quote!(#ty)
555 );
556 let try_expr = quote_spanned! {span=>
557 ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident)
558 };
559 self.conversion_stmt(try_expr, &error_msg, ident, ty, span)
560 }
561 };
562 (vec![stmt], vec![])
563 }
564 }
565 }
566
567 /// Generate conversion statements for all parameters in a function signature.
568 ///
569 /// Iterates over `inputs` (the function's parameter list) paired with `sexp_idents`
570 /// (the corresponding SEXP variable names), calling [`build_conversion`](Self::build_conversion)
571 /// for each typed parameter. Receiver parameters (`self`) are silently skipped.
572 ///
573 /// Returns a flat list of all conversion statements, in parameter order.
574 pub fn build_conversions(
575 &self,
576 inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>,
577 sexp_idents: &[syn::Ident],
578 ) -> Vec<TokenStream> {
579 let mut all_statements = Vec::new();
580
581 for (arg, sexp_ident) in inputs.iter().zip(sexp_idents.iter()) {
582 if let syn::FnArg::Typed(pat_type) = arg {
583 let statements = self.build_conversion(pat_type, sexp_ident);
584 all_statements.extend(statements);
585 }
586 }
587
588 all_statements
589 }
590}
591
592impl Default for RustConversionBuilder {
593 fn default() -> Self {
594 Self::new()
595 }
596}
597
598#[cfg(test)]
599mod tests;