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