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                // SAFETY: emitted into the wrapper's with_r_unwind_protect closure (R main thread).
100                Err(e) => return unsafe { ::miniextendr_api::error_value::make_rust_condition_value(
101                    &format!("{}: {e}", #error_msg),
102                    ::miniextendr_api::error_value::kind::CONVERSION,
103                    ::core::option::Option::None,
104                    Some(__miniextendr_call),
105                ) },
106            };
107        }
108    }
109
110    /// Like `conversion_stmt` but without a type annotation on the binding.
111    fn conversion_stmt_untyped(
112        &self,
113        try_expr: TokenStream,
114        error_msg: &str,
115        ident: &syn::Ident,
116        span: proc_macro2::Span,
117    ) -> TokenStream {
118        quote_spanned! {span=>
119            let #ident = match #try_expr {
120                Ok(v) => v,
121                // SAFETY: emitted into the wrapper's with_r_unwind_protect closure (R main thread).
122                Err(e) => return unsafe { ::miniextendr_api::error_value::make_rust_condition_value(
123                    &format!("{}: {e}", #error_msg),
124                    ::miniextendr_api::error_value::kind::CONVERSION,
125                    ::core::option::Option::None,
126                    Some(__miniextendr_call),
127                ) },
128            };
129        }
130    }
131
132    /// Generate conversion statement for a single parameter.
133    ///
134    /// This is the non-split variant: owned conversions and borrow statements are
135    /// concatenated into a single list, suitable for main-thread execution where
136    /// everything runs in the same scope.
137    ///
138    /// - `pat_type`: the typed pattern from the function signature (e.g., `x: i32`).
139    /// - `sexp_ident`: the identifier of the raw SEXP variable holding the R argument.
140    ///
141    /// Returns a flat list of `let` binding statements that convert `sexp_ident` into
142    /// the Rust type declared in `pat_type`.
143    pub fn build_conversion(
144        &self,
145        pat_type: &syn::PatType,
146        sexp_ident: &syn::Ident,
147    ) -> Vec<TokenStream> {
148        // `build_conversion` is the single-scope (main-thread) path: every statement
149        // runs inside the same `with_r_unwind_protect` closure where the argument
150        // SEXPs are live for the whole call. So `&str` can borrow R's CHARSXP pool
151        // directly — zero-copy — exactly like `&[T]` already does. The owning-`String`
152        // detour only exists to satisfy `Send` when the value must cross the worker
153        // boundary (`build_conversion_split`), which never applies here.
154        let (owned, borrowed) = self.build_conversion_split_inner(pat_type, sexp_ident, true);
155        owned.into_iter().chain(borrowed).collect()
156    }
157
158    /// Generate conversion statements split into two phases for worker thread execution.
159    ///
160    /// For reference types like `&str`, we need to:
161    /// 1. Convert SEXP to owned type (String) -- runs on the main thread before the
162    ///    worker closure, so the owned value can be moved into the closure.
163    /// 2. Borrow from the owned type (`&str`) -- runs inside the worker closure.
164    ///
165    /// For non-reference types (scalars, `Vec`, etc.) everything goes into the first
166    /// phase and the second vec is empty.
167    ///
168    /// - `pat_type`: the typed pattern from the function signature (e.g., `s: &str`).
169    /// - `sexp_ident`: the identifier of the raw SEXP variable holding the R argument.
170    ///
171    /// Returns `(owned_conversions, borrow_statements)` where each element is a list
172    /// of `let` binding token streams.
173    pub fn build_conversion_split(
174        &self,
175        pat_type: &syn::PatType,
176        sexp_ident: &syn::Ident,
177    ) -> (Vec<TokenStream>, Vec<TokenStream>) {
178        // Worker path: `&str` MUST be owned-then-borrowed because a borrowed view
179        // over R's CHARSXP pool is `!Send` and cannot move into the worker closure.
180        self.build_conversion_split_inner(pat_type, sexp_ident, false)
181    }
182
183    /// Inner implementation of [`Self::build_conversion_split`].
184    ///
185    /// `zero_copy_str` controls how a `&str` argument is lowered:
186    /// - `true` (main-thread, single-scope): emit a direct zero-copy `&str` borrow
187    ///   over R's CHARSXP pool via `TryFromSexp` — no `String` allocation. Sound
188    ///   because the SEXP outlives the borrow inside the `with_r_unwind_protect`
189    ///   closure, and the `&str`'s lifetime is tied to that scope (storing it beyond
190    ///   the call is a borrow-checker error).
191    /// - `false` (worker): convert to an owned `String` on the main thread, then
192    ///   borrow `&str` inside the worker closure — the `String` is `Send`, the
193    ///   borrowed view is not.
194    fn build_conversion_split_inner(
195        &self,
196        pat_type: &syn::PatType,
197        sexp_ident: &syn::Ident,
198        zero_copy_str: bool,
199    ) -> (Vec<TokenStream>, Vec<TokenStream>) {
200        let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() else {
201            return (vec![], vec![]);
202        };
203        let ident = &pat_ident.ident;
204        let ty = pat_type.ty.as_ref();
205
206        match ty {
207            // Unit type: ()
208            // Note: We never generate `mut` on conversion bindings - the user's function
209            // has its own parameter binding that will be `mut` if they specified it.
210            syn::Type::Tuple(t) if t.elems.is_empty() => {
211                let stmt = quote! { let #ident = (); };
212                (vec![stmt], vec![])
213            }
214
215            // Reference types: &T, &mut T
216            syn::Type::Reference(r) => {
217                let param_name = ident.to_string();
218                let is_dots = matches!(
219                    r.elem.as_ref(),
220                    syn::Type::Path(tp)
221                        if tp.path.segments.last()
222                            .map(|s| s.ident == "Dots")
223                            .unwrap_or(false)
224                );
225                let is_slice = matches!(r.elem.as_ref(), syn::Type::Slice(_));
226                let is_str = matches!(
227                    r.elem.as_ref(),
228                    syn::Type::Path(tp) if tp.path.is_ident("str")
229                );
230
231                // &[T] / &mut [T] with match_arg + several_ok:
232                // two-phase: pre-call Vec<T>, in-call borrow
233                if is_slice
234                    && self
235                        .match_arg_several_ok_params
236                        .contains(&param_name.to_string())
237                    && let Some((crate::SeveralOkContainer::BorrowedSlice, inner_ty)) =
238                        crate::classify_several_ok_container(ty)
239                {
240                    let is_mut = r.mutability.is_some();
241                    let storage_ident = quote::format_ident!("__storage_{}", ident);
242                    let error_msg = format!(
243                        "failed to convert parameter '{}' to &{}[{}]: invalid choice",
244                        param_name,
245                        if is_mut { "mut " } else { "" },
246                        quote::quote!(#inner_ty)
247                    );
248                    let vec_ty: syn::Type = syn::parse_quote!(::std::vec::Vec<#inner_ty>);
249                    let span = ty.span();
250                    let try_expr = quote_spanned! {span=>
251                        ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
252                    };
253                    // Emit owned Vec<T> binding.
254                    // For &mut [T] the storage binding needs `mut`.
255                    let owned_stmt = if is_mut {
256                        // Need `let mut storage_ident: vec_ty = ...`; inline the mut variant.
257                        let em = &error_msg;
258                        quote_spanned! {span=>
259                            let mut #storage_ident: #vec_ty = match #try_expr {
260                                Ok(v) => v,
261                                // SAFETY: emitted into the wrapper's with_r_unwind_protect closure (R main thread).
262                                Err(e) => return unsafe { ::miniextendr_api::error_value::make_rust_condition_value(
263                                    &format!("{}: {e}", #em),
264                                    ::miniextendr_api::error_value::kind::CONVERSION,
265                                    ::core::option::Option::None,
266                                    Some(__miniextendr_call),
267                                ) },
268                            };
269                        }
270                    } else {
271                        self.conversion_stmt(try_expr, &error_msg, &storage_ident, &vec_ty, span)
272                    };
273                    let borrow_stmt = if is_mut {
274                        quote_spanned! {span=>
275                            let #ident: #ty = &mut #storage_ident;
276                        }
277                    } else {
278                        quote_spanned! {span=>
279                            let #ident: #ty = &#storage_ident;
280                        }
281                    };
282                    return (vec![owned_stmt], vec![borrow_stmt]);
283                }
284
285                if is_dots {
286                    // &Dots: create wrapper with storage (main thread only - requires SEXP)
287                    let storage_ident = quote::format_ident!("{}_storage", ident);
288                    let stmt = quote! {
289                        let #storage_ident = ::miniextendr_api::dots::Dots { inner: #sexp_ident };
290                        let #ident = &#storage_ident;
291                    };
292                    (vec![stmt], vec![])
293                } else if is_slice {
294                    // &[T]: use TryFromSexp (backed by DATAPTR_RO)
295                    let error_msg = format!(
296                        "failed to convert parameter '{}' to slice: wrong type or length",
297                        ident
298                    );
299                    let span = ty.span();
300                    let try_expr = quote_spanned! {span=>
301                        ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident)
302                    };
303                    let stmt = self.conversion_stmt_untyped(try_expr, &error_msg, ident, span);
304                    (vec![stmt], vec![])
305                } else if is_str {
306                    let span = ty.span();
307                    let error_msg = format!(
308                        "failed to convert parameter '{}' to string: expected character vector",
309                        ident
310                    );
311                    if zero_copy_str {
312                        // Main-thread path: borrow R's CHARSXP pool directly via the
313                        // `&'static str` TryFromSexp impl — zero allocation. The SEXP
314                        // is a live wrapper argument for the whole call, so the borrow
315                        // is sound; its lifetime is tied to this scope, so storing it
316                        // beyond the call is a borrow-checker error (no-store guarantee).
317                        let try_expr = quote_spanned! {span=>
318                            ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident)
319                        };
320                        let stmt = self.conversion_stmt_untyped(try_expr, &error_msg, ident, span);
321                        (vec![stmt], vec![])
322                    } else {
323                        // Worker path: convert to owned String, then borrow using the
324                        // Borrow trait. The String moves into the worker closure (it is
325                        // Send); a borrowed view over R's CHARSXP pool is not.
326                        let owned_ident = quote::format_ident!("__owned_{}", ident);
327                        // Owned conversion: SEXP -> String
328                        let string_ty: syn::Type = syn::parse_quote!(String);
329                        let try_expr = quote_spanned! {span=>
330                            ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident)
331                        };
332                        let owned_stmt = self.conversion_stmt(
333                            try_expr,
334                            &error_msg,
335                            &owned_ident,
336                            &string_ty,
337                            span,
338                        );
339                        // Borrow: String -> &str (using Borrow trait)
340                        let borrow_stmt = quote_spanned! {span=>
341                            let #ident: &str = ::std::borrow::Borrow::borrow(&#owned_ident);
342                        };
343                        (vec![owned_stmt], vec![borrow_stmt])
344                    }
345                } else {
346                    // &T for other types: use TryFromSexp for the reference type.
347                    let error_msg = format!(
348                        "failed to convert parameter '{}' to {}: wrong type",
349                        ident,
350                        quote!(#ty)
351                    );
352                    let span = ty.span();
353                    let try_expr = quote_spanned! {span=>
354                        ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident)
355                    };
356                    let stmt = self.conversion_stmt(try_expr, &error_msg, ident, ty, span);
357                    (vec![stmt], vec![])
358                }
359            }
360
361            // All other types
362            _ => {
363                let param_name = ident.to_string();
364
365                // Strict mode: use checked input helpers for lossy types
366                if self.strict
367                    && let Some(strict_expr) =
368                        crate::return_type_analysis::strict_input_conversion_for_type(
369                            ty,
370                            sexp_ident,
371                            &param_name,
372                        )
373                {
374                    let span = ty.span();
375                    let stmt = quote_spanned! {span=>
376                        let #ident: #ty = #strict_expr;
377                    };
378                    return (vec![stmt], vec![]);
379                }
380
381                // match_arg + several_ok: use match_arg_vec_from_sexp for container types
382                if self
383                    .match_arg_several_ok_params
384                    .contains(&param_name.to_string())
385                    && let Some((container, inner_ty)) = crate::classify_several_ok_container(ty)
386                {
387                    let span = ty.span();
388                    match container {
389                        crate::SeveralOkContainer::Vec => {
390                            let error_msg = format!(
391                                "failed to convert parameter '{}' to Vec<{}>: invalid choice",
392                                param_name,
393                                quote!(#inner_ty)
394                            );
395                            let try_expr = quote_spanned! {span=>
396                                ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
397                            };
398                            let stmt = self.conversion_stmt(try_expr, &error_msg, ident, ty, span);
399                            return (vec![stmt], vec![]);
400                        }
401                        crate::SeveralOkContainer::BoxedSlice => {
402                            let error_msg = format!(
403                                "failed to convert parameter '{}' to Box<[{}]>: invalid choice",
404                                param_name,
405                                quote!(#inner_ty)
406                            );
407                            let try_expr = quote_spanned! {span=>
408                                ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
409                                    .map(|v| v.into_boxed_slice())
410                            };
411                            let stmt = self.conversion_stmt(try_expr, &error_msg, ident, ty, span);
412                            return (vec![stmt], vec![]);
413                        }
414                        crate::SeveralOkContainer::Array(n) => {
415                            let error_msg = format!(
416                                "failed to convert parameter '{}': invalid choice",
417                                param_name,
418                            );
419                            let param_name_lit = &param_name;
420                            let span = ty.span();
421                            // First extract the Vec via match_arg_vec_from_sexp (handles
422                            // match_arg validation + error reporting), then convert length-check
423                            // separately via a direct panic (caught by the framework).
424                            let vec_ty: syn::Type = syn::parse_quote!(::std::vec::Vec<#inner_ty>);
425                            let vec_ident = quote::format_ident!("__vec_{}", ident);
426                            let try_expr = quote_spanned! {span=>
427                                ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
428                            };
429                            let vec_stmt = self
430                                .conversion_stmt(try_expr, &error_msg, &vec_ident, &vec_ty, span);
431                            // Length check + array conversion via panic (framework catches panics)
432                            let arr_stmt = quote_spanned! {span=>
433                                let #ident: #ty = {
434                                    if #vec_ident.len() != #n {
435                                        panic!(
436                                            "parameter `{}`: expected {} values for [_; {}], got {}",
437                                            #param_name_lit, #n, #n, #vec_ident.len()
438                                        );
439                                    }
440                                    <[#inner_ty; #n]>::try_from(#vec_ident)
441                                        .unwrap_or_else(|_| unreachable!())
442                                };
443                            };
444                            return (vec![vec_stmt, arr_stmt], vec![]);
445                        }
446                        crate::SeveralOkContainer::BorrowedSlice => {
447                            let storage_ident = quote::format_ident!("__storage_{}", ident);
448                            let error_msg = format!(
449                                "failed to convert parameter '{}' to &[{}]: invalid choice",
450                                param_name,
451                                quote!(#inner_ty)
452                            );
453                            let vec_ty: syn::Type = syn::parse_quote!(::std::vec::Vec<#inner_ty>);
454                            let try_expr = quote_spanned! {span=>
455                                ::miniextendr_api::match_arg_vec_from_sexp::<#inner_ty>(#sexp_ident)
456                            };
457                            let owned_stmt = self.conversion_stmt(
458                                try_expr,
459                                &error_msg,
460                                &storage_ident,
461                                &vec_ty,
462                                span,
463                            );
464                            let borrow_stmt = quote_spanned! {span=>
465                                let #ident: #ty = &#storage_ident;
466                            };
467                            return (vec![owned_stmt], vec![borrow_stmt]);
468                        }
469                    }
470                }
471
472                let should_coerce = self.should_coerce(&param_name);
473                let coercion_mapping = if should_coerce {
474                    CoercionMapping::from_type(ty)
475                } else {
476                    None
477                };
478
479                let span = ty.span();
480                let stmt = match coercion_mapping {
481                    Some(CoercionMapping::Scalar { r_native, target }) => {
482                        let error_msg_convert = format!(
483                            "failed to convert parameter '{}' from R: wrong type",
484                            param_name
485                        );
486                        let error_msg_coerce = format!(
487                            "failed to coerce parameter '{}' to {}: overflow, NaN, or precision loss",
488                            param_name,
489                            quote!(#target)
490                        );
491                        quote_spanned! {span=>
492                            let #ident: #target = {
493                                let __r_val: #r_native = match ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident) {
494                                    Ok(v) => v,
495                                    // SAFETY: emitted into the wrapper's with_r_unwind_protect closure (R main thread).
496                                    Err(e) => return unsafe { ::miniextendr_api::error_value::make_rust_condition_value(
497                                        &format!("{}: {e}", #error_msg_convert),
498                                        ::miniextendr_api::error_value::kind::CONVERSION,
499                                        ::core::option::Option::None,
500                                        Some(__miniextendr_call),
501                                    ) },
502                                };
503                                match ::miniextendr_api::TryCoerce::<#target>::try_coerce(__r_val) {
504                                    Ok(v) => v,
505                                    // SAFETY: emitted into the wrapper's with_r_unwind_protect closure (R main thread).
506                                    Err(e) => return unsafe { ::miniextendr_api::error_value::make_rust_condition_value(
507                                        &format!("{}: {e}", #error_msg_coerce),
508                                        ::miniextendr_api::error_value::kind::CONVERSION,
509                                        ::core::option::Option::None,
510                                        Some(__miniextendr_call),
511                                    ) },
512                                }
513                            };
514                        }
515                    }
516                    Some(CoercionMapping::Vec {
517                        r_native_elem,
518                        target_elem,
519                    }) => {
520                        let error_msg_convert = format!(
521                            "failed to convert parameter '{}' to vector: wrong type",
522                            param_name
523                        );
524                        // Project principle "collect all errors in vectorized ops":
525                        // walk the whole slice and batch every failing element
526                        // (indexed) into one diagnostic via the #1192 accumulator
527                        // (`BatchedErrors`), rather than short-circuiting at the first.
528                        // Baked #1217-PR-A decision: the outer prefix carries only the
529                        // parameter name; the container label (`Vec<u32>`) is supplied to
530                        // `into_error`, and the trailing "overflow, NaN, or precision
531                        // loss" hint is dropped (each per-index `{e}` already says why).
532                        let error_msg_coerce =
533                            format!("failed to coerce parameter '{}'", param_name);
534                        let container_label = format!("Vec<{}>", quote!(#target_elem));
535                        quote_spanned! {span=>
536                            let #ident: Vec<#target_elem> = {
537                                let __r_slice: &[#r_native_elem] = match ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident) {
538                                    Ok(v) => v,
539                                    // SAFETY: emitted into the wrapper's with_r_unwind_protect closure (R main thread).
540                                    Err(e) => return unsafe { ::miniextendr_api::error_value::make_rust_condition_value(
541                                        &format!("{}: {e}", #error_msg_convert),
542                                        ::miniextendr_api::error_value::kind::CONVERSION,
543                                        ::core::option::Option::None,
544                                        Some(__miniextendr_call),
545                                    ) },
546                                };
547                                let mut __coerced: Vec<#target_elem> = Vec::with_capacity(__r_slice.len());
548                                let mut __errors = ::miniextendr_api::from_r::BatchedErrors::default();
549                                for (__i, __elem) in __r_slice.iter().copied().enumerate() {
550                                    match ::miniextendr_api::TryCoerce::<#target_elem>::try_coerce(__elem) {
551                                        Ok(__v) => __coerced.push(__v),
552                                        Err(__e) => __errors.push(|| format!("invalid value at index {__i}: {__e}")),
553                                    }
554                                }
555                                if __errors.is_empty() {
556                                    __coerced
557                                } else {
558                                    // `into_error` always yields `SexpError::InvalidValue`; take its
559                                    // inner message directly so the outer format doesn't double the
560                                    // "invalid value: " prefix that `SexpError`'s Display would add
561                                    // (mirrors `collect_coerced`'s per-element unwrap in from_r.rs).
562                                    let __batched = match __errors.into_error(#container_label) {
563                                        ::miniextendr_api::from_r::SexpError::InvalidValue(__m) => __m,
564                                        __other => ::std::string::ToString::to_string(&__other),
565                                    };
566                                    // SAFETY: emitted into the wrapper's with_r_unwind_protect closure (R main thread).
567                                    return unsafe { ::miniextendr_api::error_value::make_rust_condition_value(
568                                        &format!("{}: {__batched}", #error_msg_coerce),
569                                        ::miniextendr_api::error_value::kind::CONVERSION,
570                                        ::core::option::Option::None,
571                                        Some(__miniextendr_call),
572                                    ) };
573                                }
574                            };
575                        }
576                    }
577                    None => {
578                        let error_msg = format!(
579                            "failed to convert parameter '{}' to {}: wrong type, length, or contains NA",
580                            param_name,
581                            quote!(#ty)
582                        );
583                        let try_expr = quote_spanned! {span=>
584                            ::miniextendr_api::TryFromSexp::try_from_sexp(#sexp_ident)
585                        };
586                        self.conversion_stmt(try_expr, &error_msg, ident, ty, span)
587                    }
588                };
589                (vec![stmt], vec![])
590            }
591        }
592    }
593
594    /// Generate conversion statements for all parameters in a function signature.
595    ///
596    /// Iterates over `inputs` (the function's parameter list) paired with `sexp_idents`
597    /// (the corresponding SEXP variable names), calling [`build_conversion`](Self::build_conversion)
598    /// for each typed parameter. Receiver parameters (`self`) are silently skipped.
599    ///
600    /// Returns a flat list of all conversion statements, in parameter order.
601    pub fn build_conversions(
602        &self,
603        inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>,
604        sexp_idents: &[syn::Ident],
605    ) -> Vec<TokenStream> {
606        let mut all_statements = Vec::new();
607
608        for (arg, sexp_ident) in inputs.iter().zip(sexp_idents.iter()) {
609            if let syn::FnArg::Typed(pat_type) = arg {
610                let statements = self.build_conversion(pat_type, sexp_ident);
611                all_statements.extend(statements);
612            }
613        }
614
615        all_statements
616    }
617}
618
619impl Default for RustConversionBuilder {
620    fn default() -> Self {
621        Self::new()
622    }
623}
624
625#[cfg(test)]
626mod tests;