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, _)) = &current_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.