Skip to main content

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(&param_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(&param_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                            &param_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(&param_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 = &param_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(&param_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;