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 let (owned, borrowed) = self.build_conversion_split(pat_type, sexp_ident);
147 owned.into_iter().chain(borrowed).collect()
148 }
149
150 /// Generate conversion statements split into two phases for worker thread execution.
151 ///
152 /// For reference types like `&str`, we need to:
153 /// 1. Convert SEXP to owned type (String) -- runs on the main thread before the
154 /// worker closure, so the owned value can be moved into the closure.
155 /// 2. Borrow from the owned type (`&str`) -- runs inside the worker closure.
156 ///
157 /// For non-reference types (scalars, `Vec`, etc.) everything goes into the first
158 /// phase and the second vec is empty.
159 ///
160 /// - `pat_type`: the typed pattern from the function signature (e.g., `s: &str`).
161 /// - `sexp_ident`: the identifier of the raw SEXP variable holding the R argument.
162 ///
163 /// Returns `(owned_conversions, borrow_statements)` where each element is a list
164 /// of `let` binding token streams.
165 pub fn build_conversion_split(
166 &self,
167 pat_type: &syn::PatType,
168 sexp_ident: &syn::Ident,
169 ) -> (Vec<TokenStream>, Vec<TokenStream>) {
170 let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() else {
171 return (vec![], vec![]);
172 };
173 let ident = &pat_ident.ident;
174 let ty = pat_type.ty.as_ref();
175
176 match ty {
177 // Unit type: ()
178 // Note: We never generate `mut` on conversion bindings - the user's function
179 // has its own parameter binding that will be `mut` if they specified it.
180 syn::Type::Tuple(t) if t.elems.is_empty() => {
181 let stmt = quote! { let #ident = (); };
182 (vec![stmt], vec![])
183 }
184
185 // Reference types: &T, &mut T
186 syn::Type::Reference(r) => {
187 let param_name = ident.to_string();
188 let is_dots = matches!(
189 r.elem.as_ref(),
190 syn::Type::Path(tp)
191 if tp.path.segments.last()
192 .map(|s| s.ident == "Dots")
193 .unwrap_or(false)
194 );
195 let is_slice = matches!(r.elem.as_ref(), syn::Type::Slice(_));
196 let is_str = matches!(
197 r.elem.as_ref(),
198 syn::Type::Path(tp) if tp.path.is_ident("str")
199 );
200
201 // &[T] / &mut [T] with match_arg + several_ok:
202 // two-phase: pre-call Vec<T>, in-call borrow
203 if is_slice
204 && self
205 .match_arg_several_ok_params
206 .contains(¶m_name.to_string())
207 && let Some((crate::SeveralOkContainer::BorrowedSlice, inner_ty)) =
208 crate::classify_several_ok_container(ty)
209 {
210 let is_mut = r.mutability.is_some();
211 let storage_ident = quote::format_ident!("__storage_{}", ident);
212 let error_msg = format!(
213 "failed to convert parameter '{}' to &{}[{}]: invalid choice",
214 param_name,
215 if is_mut { "mut " } else { "" },
216 quote::quote!(#inner_ty)
217 );
218 let vec_ty: syn::Type = syn::parse_quote!(::std::vec::Vec<#inner_ty>);
219 let span = ty.span();
220 let try_expr = quote_spanned! {span=>
221 ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
222 };
223 // Emit owned Vec<T> binding.
224 // For &mut [T] the storage binding needs `mut`.
225 let owned_stmt = if is_mut {
226 // Need `let mut storage_ident: vec_ty = ...`; inline the mut variant.
227 let em = &error_msg;
228 quote_spanned! {span=>
229 let mut #storage_ident: #vec_ty = match #try_expr {
230 Ok(v) => v,
231 Err(e) => return ::miniextendr_api::error_value::make_rust_condition_value(
232 &format!("{}: {e}", #em),
233 ::miniextendr_api::error_value::kind::CONVERSION,
234 ::core::option::Option::None,
235 Some(__miniextendr_call),
236 ),
237 };
238 }
239 } else {
240 self.conversion_stmt(try_expr, &error_msg, &storage_ident, &vec_ty, span)
241 };
242 let borrow_stmt = if is_mut {
243 quote_spanned! {span=>
244 let #ident: #ty = &mut #storage_ident;
245 }
246 } else {
247 quote_spanned! {span=>
248 let #ident: #ty = &#storage_ident;
249 }
250 };
251 return (vec![owned_stmt], vec![borrow_stmt]);
252 }
253
254 if is_dots {
255 // &Dots: create wrapper with storage (main thread only - requires SEXP)
256 let storage_ident = quote::format_ident!("{}_storage", ident);
257 let stmt = quote! {
258 let #storage_ident = ::miniextendr_api::dots::Dots { inner: #sexp_ident };
259 let #ident = &#storage_ident;
260 };
261 (vec![stmt], vec![])
262 } else if is_slice {
263 // &[T]: use TryFromSexp (backed by DATAPTR_RO)
264 let error_msg = format!(
265 "failed to convert parameter '{}' to slice: wrong type or length",
266 ident
267 );
268 let span = ty.span();
269 let try_expr = quote_spanned! {span=>
270 ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident)
271 };
272 let stmt = self.conversion_stmt_untyped(try_expr, &error_msg, ident, span);
273 (vec![stmt], vec![])
274 } else if is_str {
275 // &str: Convert to String, then borrow using Borrow trait.
276 // This allows the String to be moved into worker thread closures.
277 let owned_ident = quote::format_ident!("__owned_{}", ident);
278 let error_msg = format!(
279 "failed to convert parameter '{}' to string: expected character vector",
280 ident
281 );
282 let span = ty.span();
283 // Owned conversion: SEXP -> String
284 let string_ty: syn::Type = syn::parse_quote!(String);
285 let try_expr = quote_spanned! {span=>
286 ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident)
287 };
288 let owned_stmt =
289 self.conversion_stmt(try_expr, &error_msg, &owned_ident, &string_ty, span);
290 // Borrow: String -> &str (using Borrow trait)
291 let borrow_stmt = quote_spanned! {span=>
292 let #ident: &str = ::std::borrow::Borrow::borrow(&#owned_ident);
293 };
294 (vec![owned_stmt], vec![borrow_stmt])
295 } else {
296 // &T for other types: use TryFromSexp for the reference type.
297 let error_msg = format!(
298 "failed to convert parameter '{}' to {}: wrong type",
299 ident,
300 quote!(#ty)
301 );
302 let span = ty.span();
303 let try_expr = quote_spanned! {span=>
304 ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident)
305 };
306 let stmt = self.conversion_stmt(try_expr, &error_msg, ident, ty, span);
307 (vec![stmt], vec![])
308 }
309 }
310
311 // All other types
312 _ => {
313 let param_name = ident.to_string();
314
315 // Strict mode: use checked input helpers for lossy types
316 if self.strict
317 && let Some(strict_expr) =
318 crate::return_type_analysis::strict_input_conversion_for_type(
319 ty,
320 sexp_ident,
321 ¶m_name,
322 )
323 {
324 let span = ty.span();
325 let stmt = quote_spanned! {span=>
326 let #ident: #ty = #strict_expr;
327 };
328 return (vec![stmt], vec![]);
329 }
330
331 // match_arg + several_ok: use match_arg_vec_from_sexp for container types
332 if self
333 .match_arg_several_ok_params
334 .contains(¶m_name.to_string())
335 && let Some((container, inner_ty)) = crate::classify_several_ok_container(ty)
336 {
337 let span = ty.span();
338 match container {
339 crate::SeveralOkContainer::Vec => {
340 let error_msg = format!(
341 "failed to convert parameter '{}' to Vec<{}>: invalid choice",
342 param_name,
343 quote!(#inner_ty)
344 );
345 let try_expr = quote_spanned! {span=>
346 ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
347 };
348 let stmt = self.conversion_stmt(try_expr, &error_msg, ident, ty, span);
349 return (vec![stmt], vec![]);
350 }
351 crate::SeveralOkContainer::BoxedSlice => {
352 let error_msg = format!(
353 "failed to convert parameter '{}' to Box<[{}]>: invalid choice",
354 param_name,
355 quote!(#inner_ty)
356 );
357 let try_expr = quote_spanned! {span=>
358 ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
359 .map(|v| v.into_boxed_slice())
360 };
361 let stmt = self.conversion_stmt(try_expr, &error_msg, ident, ty, span);
362 return (vec![stmt], vec![]);
363 }
364 crate::SeveralOkContainer::Array(n) => {
365 let error_msg = format!(
366 "failed to convert parameter '{}': invalid choice",
367 param_name,
368 );
369 let param_name_lit = ¶m_name;
370 let span = ty.span();
371 // First extract the Vec via match_arg_vec_from_sexp (handles
372 // match_arg validation + error reporting), then convert length-check
373 // separately via a direct panic (caught by the framework).
374 let vec_ty: syn::Type = syn::parse_quote!(::std::vec::Vec<#inner_ty>);
375 let vec_ident = quote::format_ident!("__vec_{}", ident);
376 let try_expr = quote_spanned! {span=>
377 ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
378 };
379 let vec_stmt = self
380 .conversion_stmt(try_expr, &error_msg, &vec_ident, &vec_ty, span);
381 // Length check + array conversion via panic (framework catches panics)
382 let arr_stmt = quote_spanned! {span=>
383 let #ident: #ty = {
384 if #vec_ident.len() != #n {
385 panic!(
386 "parameter `{}`: expected {} values for [_; {}], got {}",
387 #param_name_lit, #n, #n, #vec_ident.len()
388 );
389 }
390 <[#inner_ty; #n]>::try_from(#vec_ident)
391 .unwrap_or_else(|_| unreachable!())
392 };
393 };
394 return (vec![vec_stmt, arr_stmt], vec![]);
395 }
396 crate::SeveralOkContainer::BorrowedSlice => {
397 let storage_ident = quote::format_ident!("__storage_{}", ident);
398 let error_msg = format!(
399 "failed to convert parameter '{}' to &[{}]: invalid choice",
400 param_name,
401 quote!(#inner_ty)
402 );
403 let vec_ty: syn::Type = syn::parse_quote!(::std::vec::Vec<#inner_ty>);
404 let try_expr = quote_spanned! {span=>
405 ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
406 };
407 let owned_stmt = self.conversion_stmt(
408 try_expr,
409 &error_msg,
410 &storage_ident,
411 &vec_ty,
412 span,
413 );
414 let borrow_stmt = quote_spanned! {span=>
415 let #ident: #ty = &#storage_ident;
416 };
417 return (vec![owned_stmt], vec![borrow_stmt]);
418 }
419 }
420 }
421
422 let should_coerce = self.should_coerce(¶m_name);
423 let coercion_mapping = if should_coerce {
424 CoercionMapping::from_type(ty)
425 } else {
426 None
427 };
428
429 let span = ty.span();
430 let stmt = match coercion_mapping {
431 Some(CoercionMapping::Scalar { r_native, target }) => {
432 let error_msg_convert = format!(
433 "failed to convert parameter '{}' from R: wrong type",
434 param_name
435 );
436 let error_msg_coerce = format!(
437 "failed to coerce parameter '{}' to {}: overflow, NaN, or precision loss",
438 param_name,
439 quote!(#target)
440 );
441 quote_spanned! {span=>
442 let #ident: #target = {
443 let __r_val: #r_native = match ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident) {
444 Ok(v) => v,
445 Err(e) => return ::miniextendr_api::error_value::make_rust_condition_value(
446 &format!("{}: {e}", #error_msg_convert),
447 ::miniextendr_api::error_value::kind::CONVERSION,
448 ::core::option::Option::None,
449 Some(__miniextendr_call),
450 ),
451 };
452 match ::miniextendr_api::TryCoerce::<#target>::try_coerce(__r_val) {
453 Ok(v) => v,
454 Err(e) => return ::miniextendr_api::error_value::make_rust_condition_value(
455 &format!("{}: {e}", #error_msg_coerce),
456 ::miniextendr_api::error_value::kind::CONVERSION,
457 ::core::option::Option::None,
458 Some(__miniextendr_call),
459 ),
460 }
461 };
462 }
463 }
464 Some(CoercionMapping::Vec {
465 r_native_elem,
466 target_elem,
467 }) => {
468 let error_msg_convert = format!(
469 "failed to convert parameter '{}' to vector: wrong type",
470 param_name
471 );
472 let error_msg_coerce = format!(
473 "failed to coerce parameter '{}' to Vec<{}>: element overflow, NaN, or precision loss",
474 param_name,
475 quote!(#target_elem)
476 );
477 quote_spanned! {span=>
478 let #ident: Vec<#target_elem> = {
479 let __r_slice: &[#r_native_elem] = match ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident) {
480 Ok(v) => v,
481 Err(e) => return ::miniextendr_api::error_value::make_rust_condition_value(
482 &format!("{}: {e}", #error_msg_convert),
483 ::miniextendr_api::error_value::kind::CONVERSION,
484 ::core::option::Option::None,
485 Some(__miniextendr_call),
486 ),
487 };
488 match __r_slice.iter().copied()
489 .map(::miniextendr_api::TryCoerce::<#target_elem>::try_coerce)
490 .collect::<Result<Vec<_>, _>>()
491 {
492 Ok(v) => v,
493 Err(e) => return ::miniextendr_api::error_value::make_rust_condition_value(
494 &format!("{}: {e}", #error_msg_coerce),
495 ::miniextendr_api::error_value::kind::CONVERSION,
496 ::core::option::Option::None,
497 Some(__miniextendr_call),
498 ),
499 }
500 };
501 }
502 }
503 None => {
504 let error_msg = format!(
505 "failed to convert parameter '{}' to {}: wrong type, length, or contains NA",
506 param_name,
507 quote!(#ty)
508 );
509 let try_expr = quote_spanned! {span=>
510 ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident)
511 };
512 self.conversion_stmt(try_expr, &error_msg, ident, ty, span)
513 }
514 };
515 (vec![stmt], vec![])
516 }
517 }
518 }
519
520 /// Generate conversion statements for all parameters in a function signature.
521 ///
522 /// Iterates over `inputs` (the function's parameter list) paired with `sexp_idents`
523 /// (the corresponding SEXP variable names), calling [`build_conversion`](Self::build_conversion)
524 /// for each typed parameter. Receiver parameters (`self`) are silently skipped.
525 ///
526 /// Returns a flat list of all conversion statements, in parameter order.
527 pub fn build_conversions(
528 &self,
529 inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>,
530 sexp_idents: &[syn::Ident],
531 ) -> Vec<TokenStream> {
532 let mut all_statements = Vec::new();
533
534 for (arg, sexp_ident) in inputs.iter().zip(sexp_idents.iter()) {
535 if let syn::FnArg::Typed(pat_type) = arg {
536 let statements = self.build_conversion(pat_type, sexp_ident);
537 all_statements.extend(statements);
538 }
539 }
540
541 all_statements
542 }
543}
544
545impl Default for RustConversionBuilder {
546 fn default() -> Self {
547 Self::new()
548 }
549}
550
551#[cfg(test)]
552mod tests;