1use crate::miniextendr_impl::ClassSystem;
4use crate::type_inspect::first_type_argument;
5use proc_macro2::TokenStream;
6use quote::quote;
7use syn::{GenericArgument, PathArguments, ReturnType, Type};
8
9#[derive(#[automatically_derived]
impl ::core::fmt::Debug for Container {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
Container::Option => "Option",
Container::Result => "Result",
Container::Vec => "Vec",
})
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for Container { }
#[automatically_derived]
impl ::core::clone::Clone for Container {
#[inline]
fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for Container { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for Container { }
#[automatically_derived]
impl ::core::cmp::PartialEq for Container {
#[inline]
fn eq(&self, other: &Self) -> bool {
::core::intrinsics::discriminant_value(self) ==
::core::intrinsics::discriminant_value(other)
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for Container { }Eq)]
10enum Container {
11 Option,
12 Result,
13 Vec,
14}
15
16#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ConversionKind {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
ConversionKind::To => "To",
ConversionKind::From => "From",
})
}
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for ConversionKind { }
#[automatically_derived]
impl ::core::clone::Clone for ConversionKind {
#[inline]
fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for ConversionKind { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for ConversionKind { }
#[automatically_derived]
impl ::core::cmp::PartialEq for ConversionKind {
#[inline]
fn eq(&self, other: &Self) -> bool {
::core::intrinsics::discriminant_value(self) ==
::core::intrinsics::discriminant_value(other)
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for ConversionKind { }Eq)]
17pub(crate) enum ConversionKind {
18 To,
19 From,
20}
21
22#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ReturnWrap {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field5_finish(f, "ReturnWrap",
"system", &self.system, "target", &self.target, "containers",
&self.containers, "typed", &self.typed, "conversion",
&&self.conversion)
}
}Debug, #[automatically_derived]
impl ::core::clone::Clone for ReturnWrap {
#[inline]
fn clone(&self) -> Self {
ReturnWrap {
system: ::core::clone::Clone::clone(&self.system),
target: ::core::clone::Clone::clone(&self.target),
containers: ::core::clone::Clone::clone(&self.containers),
typed: ::core::clone::Clone::clone(&self.typed),
conversion: ::core::clone::Clone::clone(&self.conversion),
}
}
}Clone)]
23pub struct ReturnWrap {
24 system: ClassSystem,
25 target: String,
26 containers: Vec<Container>,
27 typed: bool,
28 pub(crate) conversion: Option<ConversionKind>,
29}
30
31pub(crate) fn parse_system(value: &syn::LitStr) -> syn::Result<ClassSystem> {
32 value
33 .value()
34 .parse()
35 .map_err(|message| syn::Error::new(value.span(), message))
36}
37
38fn marker_system(name: &syn::Ident) -> Option<ClassSystem> {
39 Some(match name.to_string().as_str() {
40 "WrapAsR6" => ClassSystem::R6,
41 "WrapAsS7" | "ConvertTo" | "ConvertFrom" => ClassSystem::S7,
42 "WrapAsS4" => ClassSystem::S4,
43 "WrapAsS3" => ClassSystem::S3,
44 "WrapAsEnv" => ClassSystem::Env,
45 "WrapAsVctrs" => ClassSystem::Vctrs,
46 _ => return None,
47 })
48}
49
50pub(crate) fn contains_marker(ty: &Type) -> bool {
51 match ty {
52 Type::Path(path) => path.path.segments.iter().any(|segment| {
53 marker_system(&segment.ident).is_some()
54 || match &segment.arguments {
55 PathArguments::AngleBracketed(args) => args
56 .args
57 .iter()
58 .any(|arg| #[allow(non_exhaustive_omitted_patterns)] match arg {
GenericArgument::Type(ty) if contains_marker(ty) => true,
_ => false,
}matches!(arg, GenericArgument::Type(ty) if contains_marker(ty))),
59 _ => false,
60 }
61 }),
62 Type::Reference(ty) => contains_marker(&ty.elem),
63 Type::Paren(ty) => contains_marker(&ty.elem),
64 Type::Group(ty) => contains_marker(&ty.elem),
65 Type::Slice(ty) => contains_marker(&ty.elem),
66 Type::Array(ty) => contains_marker(&ty.elem),
67 Type::Tuple(ty) => ty.elems.iter().any(contains_marker),
68 _ => false,
69 }
70}
71
72pub(crate) fn resolve(
75 output: &ReturnType,
76 attribute: Option<ClassSystem>,
77) -> syn::Result<(Option<ReturnWrap>, ReturnType)> {
78 let ReturnType::Type(arrow, ty) = output else {
79 if attribute.is_some() {
80 return Err(syn::Error::new_spanned(
81 output,
82 "`wrap` requires a named class return value",
83 ));
84 }
85 return Ok((None, output.clone()));
86 };
87 if attribute.is_none() && !contains_marker(ty) {
88 return Ok((None, output.clone()));
89 }
90 let mut leaf = ty.as_ref();
91 let mut containers = Vec::new();
92 while let Type::Path(path) = leaf {
93 let segment = path.path.segments.last().expect("type path has a segment");
94 let container = match segment.ident.to_string().as_str() {
95 "Option" => Container::Option,
96 "Result" => Container::Result,
97 "Vec" => Container::Vec,
98 _ => break,
99 };
100 let PathArguments::AngleBracketed(args) = &segment.arguments else {
101 break;
102 };
103 if container == Container::Result
104 && let Some(GenericArgument::Type(error)) = args.args.iter().nth(1)
105 {
106 if contains_marker(error) {
107 return Err(syn::Error::new_spanned(
108 error,
109 "WrapAs* belongs on the successful Result payload",
110 ));
111 }
112 if #[allow(non_exhaustive_omitted_patterns)] match error {
Type::Tuple(tuple) if tuple.elems.is_empty() => true,
_ => false,
}matches!(error, Type::Tuple(tuple) if tuple.elems.is_empty()) {
113 return Err(syn::Error::new_spanned(
114 error,
115 "explicit class wrapping does not support Result<T, ()>; use a non-unit error type",
116 ));
117 }
118 }
119 let Some(GenericArgument::Type(inner)) = args.args.first() else {
120 break;
121 };
122 containers.push(container);
123 leaf = inner;
124 }
125 if !#[allow(non_exhaustive_omitted_patterns)] match containers.as_slice() {
[] | [Container::Option] | [Container::Result] | [Container::Vec] |
[Container::Option, Container::Vec] |
[Container::Result, Container::Vec] => true,
_ => false,
}matches!(
126 containers.as_slice(),
127 [] | [Container::Option]
128 | [Container::Result]
129 | [Container::Vec]
130 | [Container::Option, Container::Vec]
131 | [Container::Result, Container::Vec]
132 ) {
133 return Err(syn::Error::new_spanned(
134 ty,
135 "explicit class wrapping supports T, Option<T>, Result<T, E>, Vec<T>, Option<Vec<T>>, or Result<Vec<T>, E>",
136 ));
137 }
138 let marker = if let Type::Path(path) = leaf {
139 path.path
140 .segments
141 .last()
142 .and_then(|segment| marker_system(&segment.ident).map(|system| (system, segment)))
143 } else {
144 None
145 };
146 let conversion =
147 marker
148 .as_ref()
149 .and_then(|(_, segment)| match segment.ident.to_string().as_str() {
150 "ConvertTo" => Some(ConversionKind::To),
151 "ConvertFrom" => Some(ConversionKind::From),
152 _ => None,
153 });
154 let typed = marker.is_some();
155 let (system, payload) = match marker {
156 Some((system, segment)) => {
157 if attribute.is_some_and(|attribute| attribute != system) {
158 return Err(syn::Error::new_spanned(
159 ty,
160 "the WrapAs* marker and `wrap` attribute select different class systems",
161 ));
162 }
163 let inner = first_type_argument(segment)
164 .ok_or_else(|| {
165 syn::Error::new_spanned(segment, "WrapAs* requires one class type argument")
166 })?
167 .clone();
168 if !#[allow(non_exhaustive_omitted_patterns)] match &segment.arguments {
PathArguments::AngleBracketed(args) if args.args.len() == 1 => true,
_ => false,
}matches!(&segment.arguments, PathArguments::AngleBracketed(args) if args.args.len() == 1)
169 {
170 return Err(syn::Error::new_spanned(
171 segment,
172 "WrapAs* requires exactly one class type argument",
173 ));
174 }
175 (system, inner)
176 }
177 None => (attribute.ok_or_else(|| syn::Error::new_spanned(ty, "place WrapAs* directly around the class payload, inside Option, Result, or Vec"))?, leaf.clone()),
178 };
179 let leaf = crate::type_inspect::peel_external_ptr(&payload);
182 let Type::Path(path) = leaf else {
183 return Err(syn::Error::new_spanned(
184 leaf,
185 "explicit wrapping requires an owned, named class type",
186 ));
187 };
188 let segment = path.path.segments.last().expect("type path has a segment");
189 if !#[allow(non_exhaustive_omitted_patterns)] match segment.arguments {
PathArguments::None => true,
_ => false,
}matches!(segment.arguments, PathArguments::None) || contains_marker(leaf) {
190 return Err(syn::Error::new_spanned(
191 leaf,
192 "WrapAs* must be closest to the named class type; put containers and visibility outside it",
193 ));
194 }
195 let target = crate::naming::ident_name(&segment.ident);
196 let mut peeled = (**ty).clone();
198 let mut slot = &mut peeled;
199 for _ in &containers {
200 let Type::Path(path) = slot else {
201 {
::core::panicking::panic_fmt(format_args!("internal error: entered unreachable code: {0}",
format_args!("validated container path")));
}unreachable!("validated container path")
202 };
203 let PathArguments::AngleBracketed(args) = &mut path
204 .path
205 .segments
206 .last_mut()
207 .expect("validated path")
208 .arguments
209 else {
210 {
::core::panicking::panic_fmt(format_args!("internal error: entered unreachable code: {0}",
format_args!("validated container arguments")));
}unreachable!("validated container arguments")
211 };
212 let GenericArgument::Type(inner) = args.args.first_mut().expect("validated payload") else {
213 {
::core::panicking::panic_fmt(format_args!("internal error: entered unreachable code: {0}",
format_args!("validated type argument")));
}unreachable!("validated type argument")
214 };
215 slot = inner;
216 }
217 *slot = payload;
218
219 Ok((
220 Some(ReturnWrap {
221 system,
222 target,
223 containers,
224 typed,
225 conversion,
226 }),
227 ReturnType::Type(*arrow, Box::new(peeled)),
228 ))
229}
230
231impl ReturnWrap {
232 pub(crate) fn system(&self) -> ClassSystem {
233 self.system
234 }
235 pub(crate) fn target(&self) -> &str {
236 &self.target
237 }
238 pub(crate) fn set_target(&mut self, target: String) {
239 self.target = target;
240 }
241 pub(crate) fn has_vector(&self) -> bool {
242 self.containers.contains(&Container::Vec)
243 }
244
245 pub(crate) fn is_optional_class(&self) -> bool {
250 self.containers == [Container::Option]
251 }
252
253 pub(crate) fn prepare_value(&self, call: TokenStream) -> TokenStream {
255 fn map(call: TokenStream, containers: &[Container]) -> TokenStream {
256 match containers.split_first() {
257 None => {
let mut _s = ::quote::__private::TokenStream::new();
::quote::__private::push_group(&mut _s,
::quote::__private::Delimiter::Parenthesis,
{
let mut _s = ::quote::__private::TokenStream::new();
::quote::ToTokens::to_tokens(&call, &mut _s);
_s
});
::quote::__private::push_dot(&mut _s);
::quote::__private::parse(&mut _s, "0");
_s
}quote!((#call).0),
258 Some((container, rest)) => {
259 let inner = map({
let mut _s = ::quote::__private::TokenStream::new();
::quote::__private::push_ident(&mut _s, "__mx_value");
_s
}quote!(__mx_value), rest);
260 if *container == Container::Vec {
261 {
let mut _s = ::quote::__private::TokenStream::new();
::quote::__private::push_group(&mut _s,
::quote::__private::Delimiter::Parenthesis,
{
let mut _s = ::quote::__private::TokenStream::new();
::quote::ToTokens::to_tokens(&call, &mut _s);
_s
});
::quote::__private::push_dot(&mut _s);
::quote::__private::push_ident(&mut _s, "into_iter");
::quote::__private::push_group(&mut _s,
::quote::__private::Delimiter::Parenthesis,
::quote::__private::TokenStream::new());
::quote::__private::push_dot(&mut _s);
::quote::__private::push_ident(&mut _s, "map");
::quote::__private::push_group(&mut _s,
::quote::__private::Delimiter::Parenthesis,
{
let mut _s = ::quote::__private::TokenStream::new();
::quote::__private::push_or(&mut _s);
::quote::__private::push_ident(&mut _s, "__mx_value");
::quote::__private::push_or(&mut _s);
::quote::ToTokens::to_tokens(&inner, &mut _s);
_s
});
::quote::__private::push_dot(&mut _s);
::quote::__private::push_ident(&mut _s, "collect");
::quote::__private::push_colon2(&mut _s);
::quote::__private::push_lt(&mut _s);
::quote::__private::push_colon2(&mut _s);
::quote::__private::push_ident(&mut _s, "std");
::quote::__private::push_colon2(&mut _s);
::quote::__private::push_ident(&mut _s, "vec");
::quote::__private::push_colon2(&mut _s);
::quote::__private::push_ident(&mut _s, "Vec");
::quote::__private::push_lt(&mut _s);
::quote::__private::push_underscore(&mut _s);
::quote::__private::push_shr(&mut _s);
::quote::__private::push_group(&mut _s,
::quote::__private::Delimiter::Parenthesis,
::quote::__private::TokenStream::new());
_s
}quote!((#call).into_iter().map(|__mx_value| #inner).collect::<::std::vec::Vec<_>>())
262 } else {
263 {
let mut _s = ::quote::__private::TokenStream::new();
::quote::__private::push_group(&mut _s,
::quote::__private::Delimiter::Parenthesis,
{
let mut _s = ::quote::__private::TokenStream::new();
::quote::ToTokens::to_tokens(&call, &mut _s);
_s
});
::quote::__private::push_dot(&mut _s);
::quote::__private::push_ident(&mut _s, "map");
::quote::__private::push_group(&mut _s,
::quote::__private::Delimiter::Parenthesis,
{
let mut _s = ::quote::__private::TokenStream::new();
::quote::__private::push_or(&mut _s);
::quote::__private::push_ident(&mut _s, "__mx_value");
::quote::__private::push_or(&mut _s);
::quote::ToTokens::to_tokens(&inner, &mut _s);
_s
});
_s
}quote!((#call).map(|__mx_value| #inner))
264 }
265 }
266 }
267 }
268 if self.typed {
269 map(call, &self.containers)
270 } else {
271 call
272 }
273 }
274
275 pub(crate) fn r_expression(&self, value: &str, self_type: Option<&str>) -> String {
277 let target = if self.target == "Self" {
278 self_type.unwrap_or("Self")
279 } else {
280 &self.target
281 };
282 let wrap = |value: &str| match self.system {
283 ClassSystem::R6 => ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}$new(.ptr = {1})", target,
value))
})format!("{target}$new(.ptr = {value})"),
284 ClassSystem::S7 => ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}(.ptr = {1})", target, value))
})format!("{target}(.ptr = {value})"),
285 ClassSystem::S4 => ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("methods::new(\"{0}\", ptr = {1})",
target, value))
})format!("methods::new(\"{target}\", ptr = {value})"),
286 ClassSystem::S3 | ClassSystem::Env => {
287 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("structure({0}, class = \"{1}\")",
value, target))
})format!("structure({value}, class = \"{target}\")")
288 }
289 ClassSystem::Vctrs => {
290 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("structure({0}, class = unique(c(\"{1}\", class({0}))))",
value, target))
})format!("structure({value}, class = unique(c(\"{target}\", class({value}))))")
291 }
292 };
293 let wrapped = if self.containers.contains(&Container::Vec) {
294 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("lapply({1}, function(.item) {0})",
wrap(".item"), value))
})format!("lapply({value}, function(.item) {})", wrap(".item"))
295 } else {
296 wrap(value)
297 };
298 if self.containers.first() == Some(&Container::Option) {
301 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("if (is.null({0})) NULL else {1}",
value, wrapped))
})format!("if (is.null({value})) NULL else {wrapped}")
302 } else {
303 wrapped
304 }
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use quote::ToTokens;
312
313 #[test]
314 fn marker_and_attribute_share_each_system_and_container_plan() {
315 for (marker, system) in [
316 ("WrapAsR6", ClassSystem::R6),
317 ("WrapAsS7", ClassSystem::S7),
318 ("WrapAsS4", ClassSystem::S4),
319 ("WrapAsS3", ClassSystem::S3),
320 ("WrapAsEnv", ClassSystem::Env),
321 ("WrapAsVctrs", ClassSystem::Vctrs),
322 ] {
323 for shape in [
324 "{}",
325 "Option<{}>",
326 "Result<{}, String>",
327 "Vec<{}>",
328 "Option<Vec<{}>>",
329 "Result<Vec<{}>, String>",
330 ] {
331 let typed: ReturnType = syn::parse_str(&format!(
332 "-> {}",
333 shape.replace("{}", &format!("miniextendr_api::{marker}<Board>"))
334 ))
335 .unwrap();
336 let plain: ReturnType =
337 syn::parse_str(&format!("-> {}", shape.replace("{}", "Board"))).unwrap();
338 let (a, output) = resolve(&typed, None).unwrap();
339 let (b, _) = resolve(&plain, Some(system)).unwrap();
340 assert_eq!(
341 output.to_token_stream().to_string(),
342 plain.to_token_stream().to_string()
343 );
344 assert_eq!(
345 a.unwrap().r_expression(".val", None),
346 b.unwrap().r_expression(".val", None)
347 );
348 }
349 }
350 }
351 #[test]
353 fn optional_wraps_let_null_through() {
354 for (shape, guarded) in [
355 ("{}", false),
356 ("Option<{}>", true),
357 ("Result<{}, String>", false),
358 ("Vec<{}>", false),
359 ("Option<Vec<{}>>", true),
360 ("Result<Vec<{}>, String>", false),
361 ] {
362 let output: ReturnType =
363 syn::parse_str(&format!("-> {}", shape.replace("{}", "WrapAsR6<Board>"))).unwrap();
364 let (plan, _) = resolve(&output, None).unwrap();
365 let plan = plan.unwrap();
366 assert_eq!(plan.is_optional_class(), shape == "Option<{}>", "{shape}");
367 let expr = plan.r_expression(".val", None);
368 assert_eq!(
369 expr.starts_with("if (is.null(.val)) NULL else "),
370 guarded,
371 "{shape}: {expr}"
372 );
373 }
374 }
375
376 #[test]
377 fn rejects_misplaced_markers_and_conflicting_systems() {
378 for (output, message) in [
379 ("-> WrapAsR6<Vec<Board>>", "closest to the named class"),
380 (
381 "-> Result<Board, WrapAsR6<Problem>>",
382 "successful Result payload",
383 ),
384 (
385 "-> Option<Option<WrapAsR6<Board>>>",
386 "supports T, Option<T>",
387 ),
388 ("-> Result<WrapAsR6<Board>, ()>", "non-unit error type"),
389 (
390 "-> (WrapAsR6<Board>, i32)",
391 "directly around the class payload",
392 ),
393 ("-> WrapAsR6<&Board>", "owned, named class type"),
394 ("-> WrapAsR6<WrapAsS7<Board>>", "closest to the named class"),
395 ] {
396 let output = syn::parse_str(output).unwrap();
397 assert!(
398 resolve(&output, None)
399 .unwrap_err()
400 .to_string()
401 .contains(message)
402 );
403 }
404 let output = syn::parse_quote!(-> WrapAsR6<Board>);
405 assert!(
406 resolve(&output, Some(ClassSystem::S7))
407 .unwrap_err()
408 .to_string()
409 .contains("different class systems")
410 );
411 assert!(resolve(&output, Some(ClassSystem::R6)).is_ok());
412 }
413
414 #[test]
417 fn external_ptr_payload_names_its_class() {
418 for (output, attribute, peeled) in [
419 (
420 "-> WrapAsR6<ExternalPtr<Board>>",
421 None,
422 "-> ExternalPtr<Board>",
423 ),
424 (
425 "-> ExternalPtr<Board>",
426 Some(ClassSystem::R6),
427 "-> ExternalPtr<Board>",
428 ),
429 (
430 "-> Option<Vec<WrapAsR6<ExternalPtr<Board>>>>",
431 None,
432 "-> Option<Vec<ExternalPtr<Board>>>",
433 ),
434 ] {
435 let (plan, output) = resolve(&syn::parse_str(output).unwrap(), attribute).unwrap();
436 let expected: ReturnType = syn::parse_str(peeled).unwrap();
437 assert_eq!(
438 output.to_token_stream().to_string(),
439 expected.to_token_stream().to_string()
440 );
441 assert!(
442 plan.unwrap()
443 .r_expression(".val", None)
444 .contains("Board$new(.ptr = ")
445 );
446 }
447 }
448
449 #[test]
450 fn explicit_self_and_qualified_targets_do_not_use_registry_placeholders() {
451 for (output, target) in [
452 ("-> WrapAsR6<Self>", "ActualClass"),
453 ("-> WrapAsR6<other::Board>", "Board"),
454 ] {
455 let (plan, _) = resolve(&syn::parse_str(output).unwrap(), None).unwrap();
456 assert_eq!(
457 plan.unwrap().r_expression(".val", Some("ActualClass")),
458 format!("{target}$new(.ptr = .val)")
459 );
460 }
461 }
462}