extendr_macros/extendr_impl.rs
1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{ItemFn, ItemImpl};
4
5#[allow(unused_imports)]
6use crate::extendr;
7use crate::extendr_options::ExtendrOptions;
8use crate::wrappers;
9
10/// Transform the method documentation to Roxygen format.
11fn transform_method_doc_roxygen(method_name: &str, doc: &str) -> (String, Vec<String>) {
12 let mut description = Vec::new();
13 let mut other_groups: Vec<(String, Vec<String>)> = Vec::new();
14 let mut current_other: Option<(String, Vec<String>)> = None;
15 let mut params: Vec<(String, Vec<String>)> = Vec::new();
16 let mut current_param: Option<(String, Vec<String>)> = None;
17 let mut state = "description";
18
19 // for each line of the docstring
20 for line in doc.lines() {
21 let trimmed = line.trim();
22 // params
23 if trimmed.starts_with("@param") {
24 if let Some((name, lines)) = current_param.take() {
25 params.push((name, lines));
26 }
27 if let Some((tag, content)) = current_other.take() {
28 other_groups.push((tag, content));
29 }
30 // split tag name from the rest
31 let mut parts = trimmed.splitn(3, ' ');
32 parts.next();
33 // Extract the parameter name and description (unwrap_or handles empty cases like multiline tags)
34 // it appends multiline param docstrings later
35 let param_name = parts.next().unwrap_or("").to_string();
36 let param_desc = parts.next().unwrap_or("").to_string();
37 current_param = Some((param_name, vec![param_desc]));
38 state = "param";
39 continue;
40 // same for every other tags
41 } else if trimmed.starts_with('@') {
42 if let Some((name, lines)) = current_param.take() {
43 params.push((name, lines));
44 }
45 let mut parts = trimmed.splitn(2, ' ');
46 let tag_with_at = parts.next().unwrap_or("");
47 let tag = tag_with_at.trim_start_matches('@').to_string();
48 // flush current group if changing tag
49 if let Some((curr_tag, _)) = ¤t_other {
50 if *curr_tag != tag {
51 let (t, c) = current_other.take().unwrap();
52 other_groups.push((t, c));
53 current_other = Some((tag.clone(), Vec::new()));
54 }
55 } else {
56 current_other = Some((tag.clone(), Vec::new()));
57 }
58 // add inline content if present
59 if let Some(inline) = parts.next() {
60 let inline = inline.trim();
61 if !inline.is_empty() {
62 if let Some((_, ref mut vec)) = current_other {
63 vec.push(inline.to_string());
64 }
65 }
66 }
67 state = "other";
68 continue;
69 }
70
71 match state {
72 "description" => description.push(trimmed.to_string()),
73 // handle multiline `@...` docstrings
74 "other" => {
75 if let Some((_, ref mut vec)) = current_other {
76 vec.push(trimmed.to_string());
77 }
78 }
79 // handle multiline `@param` docstrings
80 "param" => {
81 if let Some((_, ref mut lines)) = current_param {
82 lines.push(trimmed.to_string());
83 }
84 }
85 _ => description.push(trimmed.to_string()),
86 }
87 }
88 if let Some((name, lines)) = current_param.take() {
89 params.push((name, lines));
90 }
91 if let Some((tag, content)) = current_other.take() {
92 other_groups.push((tag, content));
93 }
94
95 // creates `method` subsection (obs.: for each impl block)
96 let mut output = String::new();
97 output.push_str(&format!("\\subsection{{Method `{}`}}{{\n", method_name));
98 if !description.is_empty() {
99 output.push_str(&description.join("\n"));
100 output.push('\n');
101 }
102 if !params.is_empty() {
103 // params docstrings goes here
104 output.push_str(" \\subsection{Arguments}{\n\\describe{\n");
105 for (pname, plines) in params {
106 let param_text = plines.join(" ");
107 output.push_str(&format!("\\item{{`{}`}}{{{}}}\n", pname, param_text));
108 }
109 output.push_str("}}\n");
110 }
111 // for other docsstring, it creates a subsection for each tag
112 // usage is special because if we don't enclose it in
113 // a preformatted block, it will be be sent as one single line, e.g.:
114 // @usage
115 // #' foo(
116 // #' bar,
117 // #' baz
118 // #' )
119 // becomes
120 // @usage
121 // #' foo(bar, baz)
122 //
123 // examples are also special: they should be appended and put under @examples (not in a custom subsection)
124 // if there's another special treatment needed, it should be added here
125 let mut examples: Vec<String> = Vec::new();
126 for (tag, contents) in other_groups {
127 match tag.as_str() {
128 "examples" => {
129 examples.push(format!(
130 "## ---- Method `{}` ---- ##\n{}",
131 method_name,
132 contents.join("\n")
133 ));
134 }
135 "usage" => {
136 output.push_str(&format!(
137 " \\subsection{{{}}}{{\n \\preformatted{{\n{}\n}}\n}}\n",
138 tag,
139 contents.join("\n")
140 ));
141 }
142 other => {
143 output.push_str(&format!(
144 " \\subsection{{{}}}{{\n{}\n}}\n",
145 other,
146 contents.join("\n")
147 ));
148 }
149 }
150 }
151 output.push_str("}\n");
152 (output, examples)
153}
154
155/// Make inherent implementations available to R
156///
157/// The `extendr_impl` function is used to make inherent implementations
158/// available to R as an environment. By adding the [`macro@extendr`] attribute
159/// macro to an `impl` block (supported with `enum`s and `struct`s), the
160/// methods in the impl block are made available as functions in an
161/// environment.
162///
163///
164/// On the R side, an environment with the same name of the inherent
165/// implementation is created. The environment has functions within it
166/// that correspond to each method in the impl block. Note that in order
167/// for an impl block to be compatible with extendr (and thus R), its return
168/// type must be able to be returned to R. For example, any struct that might
169/// be returned must _also_ have an `#[extendr]` annotated impl block.
170///
171/// Example:
172/// ```rust,ignore
173/// use extendr_api::prelude::*;
174///
175/// // a struct that will be used internal the People struct
176/// #[derive(Clone, Debug, IntoDataFrameRow)]
177/// struct Person {
178/// name: String,
179/// age: i32,
180/// }
181///
182/// // This will collect people in the struct
183/// #[extendr]
184/// #[derive(Clone, Debug)]
185/// struct People(Vec<Person>);
186///
187/// #[extendr]
188/// /// @export
189/// impl People {
190/// // instantiate a new struct with an empty vector
191/// fn new() -> Self {
192/// let vec: Vec<Person> = Vec::new();
193/// Self(vec)
194/// }
195///
196/// // add a person to the internal vector
197/// fn add_person(&mut self, name: &str, age: i32) -> &mut Self {
198/// let person = Person {
199/// name: String::from(name),
200/// age: age,
201/// };
202///
203/// self.0.push(person);
204///
205/// // return self
206/// self
207/// }
208///
209/// // Convert the struct into a data.frame
210/// fn into_df(&self) -> Robj {
211/// let df = self.0.clone().into_dataframe();
212///
213/// match df {
214/// Ok(df) => df.as_robj().clone(),
215/// Err(_) => data_frame!(),
216/// }
217/// }
218///
219/// // add another `People` struct to self
220/// fn add_people(&mut self, others: &People) -> &mut Self {
221/// self.0.extend(others.0.clone().into_iter());
222/// self
223/// }
224///
225/// // create a function to print the self which can be called
226/// // from an R print method
227/// fn print_self(&self) -> String {
228/// format!("{:?}", self.0)
229/// }
230/// }
231///
232/// // Macro to generate exports.
233/// // This ensures exported functions are registered with R.
234/// // See corresponding C code in `entrypoint.c`.
235/// extendr_module! {
236/// mod testself;
237/// impl People;
238/// }
239/// ```
240pub(crate) fn extendr_impl(
241 mut item_impl: ItemImpl,
242 opts: &ExtendrOptions,
243) -> syn::Result<TokenStream> {
244 // Only `impl name { }` allowed
245 if item_impl.defaultness.is_some() {
246 return Err(syn::Error::new_spanned(
247 item_impl,
248 "default not allowed in #[extendr] impl",
249 ));
250 }
251
252 if item_impl.unsafety.is_some() {
253 return Err(syn::Error::new_spanned(
254 item_impl,
255 "unsafe not allowed in #[extendr] impl",
256 ));
257 }
258
259 if item_impl.generics.const_params().count() != 0 {
260 return Err(syn::Error::new_spanned(
261 item_impl,
262 "const params not allowed in #[extendr] impl",
263 ));
264 }
265
266 if item_impl.generics.type_params().count() != 0 {
267 return Err(syn::Error::new_spanned(
268 item_impl,
269 "type params not allowed in #[extendr] impl",
270 ));
271 }
272
273 // if item_impl.generics.lifetimes().count() != 0 {
274 // return quote! { compile_error!("lifetime params not allowed in #[extendr] impl"); }.into();
275 // }
276
277 if item_impl.generics.where_clause.is_some() {
278 return Err(syn::Error::new_spanned(
279 item_impl,
280 "where clause not allowed in #[extendr] impl",
281 ));
282 }
283
284 let self_ty = item_impl.self_ty.as_ref();
285 let self_ty_name = wrappers::type_name(self_ty);
286 let prefix = format!("{}__", self_ty_name);
287 let mut method_meta_names = Vec::new();
288
289 // Now we get struct level docs but I think it's nice to let impl level docs too
290 // that way a user can add a impl-related docstring locally, without having to bloat the struct docs
291 let impl_doc = wrappers::get_doc_string(&item_impl.attrs);
292 let struct_doc = wrappers::get_struct_doc(&self_ty_name);
293 // Since struct docs are generated by extendr_impl, just as the methods section, it'll get duplicated for each
294 // impl block. A hack to solve it without having to split both logics is to erase struct docs so subsequent impl
295 // blocks don't repeat it
296 wrappers::register_struct_doc(&self_ty_name, "");
297
298 let doc_string = if struct_doc.is_empty() {
299 impl_doc
300 } else {
301 format!("{}\n{}", struct_doc.trim_end(), impl_doc)
302 };
303
304 let mut method_docs: Vec<(String, String)> = Vec::new();
305 let mut all_examples: Vec<String> = Vec::new();
306
307 for impl_item in &item_impl.items {
308 if let syn::ImplItem::Fn(method) = impl_item {
309 let mdoc = wrappers::get_doc_string(&method.attrs);
310 if !mdoc.is_empty() {
311 let (sect, examples) =
312 transform_method_doc_roxygen(&method.sig.ident.to_string(), &mdoc);
313 method_docs.push((method.sig.ident.to_string(), sect));
314 for ex in examples {
315 all_examples.push(ex);
316 all_examples.push(String::new());
317 }
318 }
319 }
320 }
321
322 // Build a Methods section
323 // It actually creates a method section for each impl block, but Roxygen won't complain about that.
324 let methods_section = if !method_docs.is_empty() {
325 let mut sec = String::from("\n @section Methods:");
326 for (_name, doc) in &method_docs {
327 sec.push('\n');
328 sec.push_str(doc);
329 }
330 sec
331 } else {
332 String::new()
333 };
334
335 // Append single examples block if any
336 let examples_section = if !all_examples.is_empty() {
337 let mut ex = String::from("\n @examples\n");
338 for line in &all_examples {
339 ex.push_str(line);
340 ex.push('\n');
341 }
342 ex
343 } else {
344 String::new()
345 };
346
347 let full_doc = format!("{}{}{}", doc_string, methods_section, examples_section);
348
349 // Generate wrappers for methods.
350 // eg.
351 // ```rust,ignore
352 // #[no_mangle]
353 // #[allow(non_snake_case)]
354 // pub extern "C" fn wrap__Person__new() -> extendr_api::SEXP {
355 // unsafe {
356 // use extendr_api::FromRobj;
357 // extendr_api::Robj::from(<Person>::new()).get()
358 // }
359 // }
360 // ```
361 let mut wrappers: Vec<ItemFn> = Vec::new();
362 for impl_item in &mut item_impl.items {
363 if let syn::ImplItem::Fn(ref mut method) = impl_item {
364 method_meta_names.push(format_ident!(
365 "{}{}__{}",
366 wrappers::META_PREFIX,
367 self_ty_name,
368 method.sig.ident
369 ));
370 wrappers::make_function_wrappers(
371 opts,
372 &mut wrappers,
373 prefix.as_str(),
374 &method.attrs,
375 &mut method.sig,
376 Some(self_ty),
377 )?;
378 }
379 }
380
381 let meta_name = format_ident!("{}{}", wrappers::META_PREFIX, self_ty_name);
382
383 let expanded = TokenStream::from(quote! {
384 // The impl itself copied from the source.
385 #item_impl
386
387 // Function wrappers
388 #( #wrappers )*
389
390 #[allow(non_snake_case)]
391 fn #meta_name(impls: &mut Vec<extendr_api::metadata::Impl>) {
392 let mut methods = Vec::new();
393 #( #method_meta_names(&mut methods); )*
394 impls.push(extendr_api::metadata::Impl {
395 doc: #full_doc,
396 name: #self_ty_name,
397 methods,
398 });
399 }
400 });
401
402 //eprintln!("{}", expanded);
403 Ok(expanded)
404}
405
406// This structure contains parameters parsed from the #[extendr_module] definition.