miniextendr_macros/rust_conversion_builder.rs
1//! Shared utilities for converting R SEXP parameters to Rust types.
2//!
3//! This module provides a builder for generating Rust conversion code from R SEXP arguments,
4//! ensuring consistent behavior across standalone functions and impl methods.
5
6use crate::miniextendr_fn::CoercionMapping;
7use proc_macro2::TokenStream;
8use quote::{quote, quote_spanned};
9use syn::spanned::Spanned;
10
11/// Builder for generating Rust conversion statements from R SEXP parameters.
12///
13/// Handles:
14/// - Unit types `()` → identity binding
15/// - `&Dots` → special wrapper with storage
16/// - Slices `&[T]` → TryFromSexp
17/// - `&str` → String + Borrow (for worker thread compatibility)
18/// - Scalar references → DATAPTR_RO_unchecked
19/// - Coercion → extract R native type + TryCoerce
20/// - Default → TryFromSexp
21pub struct RustConversionBuilder {
22 /// Enable coercion for all parameters
23 coerce_all: bool,
24 /// Parameter names that should use coercion
25 coerce_params: Vec<String>,
26 /// Enable strict input conversion for lossy types
27 strict: bool,
28 /// Parameter names with `match_arg + several_ok` — use `match_arg_vec_from_sexp` instead of `TryFromSexp`.
29 match_arg_several_ok_params: Vec<String>,
30}
31
32impl RustConversionBuilder {
33 /// Create a new conversion builder.
34 pub fn new() -> Self {
35 Self {
36 coerce_all: false,
37 coerce_params: Vec::new(),
38 strict: false,
39 match_arg_several_ok_params: Vec::new(),
40 }
41 }
42
43 /// Enable coercion for all parameters.
44 pub fn with_coerce_all(mut self) -> Self {
45 self.coerce_all = true;
46 self
47 }
48
49 /// Add a single parameter name that should use coercion.
50 ///
51 /// `param_name` is matched against the identifier in the function signature.
52 /// Can be called multiple times to add several parameters.
53 pub fn with_coerce_param(mut self, param_name: String) -> Self {
54 self.coerce_params.push(param_name);
55 self
56 }
57
58 /// Enable strict input conversion for lossy types (i64/u64/isize/usize + Vec variants).
59 pub fn with_strict(mut self) -> Self {
60 self.strict = true;
61 self
62 }
63
64 /// Mark a parameter as `match_arg + several_ok` — uses `match_arg_vec_from_sexp`
65 /// instead of `TryFromSexp` for converting STRSXP → `Vec<EnumType>`.
66 pub fn with_match_arg_several_ok(mut self, param_name: String) -> Self {
67 self.match_arg_several_ok_params.push(param_name);
68 self
69 }
70
71 /// Check if a parameter should use coercion.
72 ///
73 /// Returns `true` if `coerce_all` is set or `param_name` appears in the per-parameter list.
74 fn should_coerce(&self, param_name: &str) -> bool {
75 self.coerce_all || self.coerce_params.contains(¶m_name.to_string())
76 }
77
78 /// Generate a conversion expression that returns a tagged condition SEXP on failure.
79 ///
80 /// The R wrapper inspects `.val` and raises a structured `rust_*` condition; the
81 /// `return` happens from inside the C wrapper body before any further conversion.
82 ///
83 /// - `try_expr`: The `Result<T, E>`-producing expression
84 /// - `error_msg`: Human-readable error message for the failure
85 /// - `ident`: The binding name for the converted value
86 /// - `ty`: The target Rust type (for the `let` binding)
87 /// - `span`: Source span for error reporting
88 fn conversion_stmt(
89 &self,
90 try_expr: TokenStream,
91 error_msg: &str,
92 ident: &syn::Ident,
93 ty: &syn::Type,
94 span: proc_macro2::Span,
95 ) -> TokenStream {
96 quote_spanned! {span=>
97 let #ident: #ty = match #try_expr {
98 Ok(v) => v,
99 // 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(¶m_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 ¶m_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(¶m_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 = ¶m_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(¶m_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;