use crate::codegen::doc_emission::{parse_arguments_bullets, parse_rustdoc_sections};
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{ApiSurface, EnumDef, ParamDef, TypeDef, TypeRef};
use std::collections::HashMap;
use super::bridges::{is_flat_data_enum, is_json_passthrough_data_enum};
use super::options::find_r_options_type_from_api;
use super::trait_bridge_wrappers::{TraitBridgeFn, collect_excluded_class_types, method_is_excluded_from_impl};
pub(super) fn r_type_description(ty: &TypeRef) -> String {
match ty {
TypeRef::Bytes => "Raw vector of bytes.".to_string(),
TypeRef::String => "Character string.".to_string(),
TypeRef::Char => "Single-character string.".to_string(),
TypeRef::Primitive(p) => match p {
crate::core::ir::PrimitiveType::Bool => "Logical (TRUE/FALSE).".to_string(),
crate::core::ir::PrimitiveType::F32 | crate::core::ir::PrimitiveType::F64 => "Numeric.".to_string(),
_ => "Integer.".to_string(),
},
TypeRef::Optional(inner) => {
let inner_desc = r_type_description(inner);
let trimmed = inner_desc.trim_end_matches('.');
let body = if matches!(**inner, TypeRef::Named(_)) {
trimmed.to_string()
} else {
match trimmed.chars().next() {
Some(c) => {
let mut s = c.to_lowercase().collect::<String>();
s.push_str(&trimmed[c.len_utf8()..]);
s
}
None => String::new(),
}
};
format!("Optional {body}. Defaults to NULL.")
}
TypeRef::Vec(inner) => {
let inner_desc = r_type_description(inner);
let trimmed = inner_desc.trim_end_matches('.');
format!("List of {}.", trimmed.to_lowercase())
}
TypeRef::Map(_, _) => "Named list.".to_string(),
TypeRef::Named(name) => format!("{name} object (list with class attribute)."),
TypeRef::Path => "File path as character string.".to_string(),
TypeRef::Unit => "Invisible NULL.".to_string(),
TypeRef::Json => "JSON-serializable value.".to_string(),
TypeRef::Duration => "Numeric duration in seconds.".to_string(),
}
}
pub(super) fn title_case_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
pub(super) fn push_roxygen_inline_multiline(block: &mut String, text: &str) {
let mut lines = text.lines();
if let Some(first) = lines.next() {
block.push_str(first.trim_end());
}
for line in lines {
block.push('\n');
block.push_str("#' ");
block.push_str(line.trim_end());
}
}
pub(super) fn r_roxygen_block(func_name: &str, doc: &str, params: &[ParamDef], return_type: &TypeRef) -> String {
let mut block = String::with_capacity(256);
let trimmed_doc = doc.trim();
let sections = parse_rustdoc_sections(trimmed_doc);
let summary = sections.summary.trim();
let (title, description) = if summary.is_empty() {
(func_name.to_string(), String::new())
} else {
let mut parts = summary.splitn(2, '\n');
let raw_title = parts.next().unwrap_or("").trim().trim_end_matches('.');
let title = title_case_first(raw_title);
let description = parts.next().map(str::trim).unwrap_or("").to_string();
(title, description)
};
block.push_str("#' ");
block.push_str(&title);
block.push('\n');
if !description.is_empty() {
block.push_str("#'\n");
for line in description.lines() {
let line = line.trim_end();
if line.is_empty() {
block.push_str("#'\n");
} else {
block.push_str("#' ");
block.push_str(line);
block.push('\n');
}
}
}
let mut param_docs: HashMap<String, String> = HashMap::new();
if let Some(args_body) = sections.arguments.as_deref() {
for (name, desc) in parse_arguments_bullets(args_body) {
if !desc.is_empty() {
param_docs.insert(name, desc);
}
}
}
for param in params {
block.push_str("#' @param ");
block.push_str(¶m.name);
block.push(' ');
if let Some(desc) = param_docs.get(¶m.name) {
push_roxygen_inline_multiline(&mut block, desc);
if !desc.trim_end().ends_with('.') {
block.push('.');
}
} else {
block.push_str(&r_type_description(¶m.ty));
}
block.push('\n');
}
block.push_str("#' @return ");
if let Some(ret) = sections.returns.as_deref() {
let ret = ret.trim();
push_roxygen_inline_multiline(&mut block, ret);
if !ret.ends_with('.') {
block.push('.');
}
} else {
block.push_str(&r_type_description(return_type));
}
block.push('\n');
if let Some(err) = sections.errors.as_deref() {
block.push_str("#'\n#' @section Errors:\n");
for line in err.trim().lines() {
let line = line.trim_end();
if line.is_empty() {
block.push_str("#'\n");
} else {
block.push_str("#' ");
block.push_str(line);
block.push('\n');
}
}
}
block.push_str("#' @export\n");
block
}
pub(super) fn r_field_one_liner(field_name: &str, doc: &str) -> String {
let trimmed = doc.trim();
if trimmed.is_empty() {
return field_name.to_string();
}
let paragraph: Vec<&str> = trimmed
.lines()
.take_while(|l| !l.trim().is_empty())
.map(str::trim)
.collect();
if paragraph.is_empty() {
field_name.to_string()
} else {
let mut result = paragraph.join(" ");
let max_desc_len = 109_usize.saturating_sub(field_name.len());
if result.len() > max_desc_len {
result.truncate(max_desc_len);
if let Some(last_space) = result.rfind(' ') {
result.truncate(last_space);
}
}
result
}
}
pub(super) fn r_class_roxygen_block(typ: &TypeDef) -> String {
let mut block = String::with_capacity(256);
let sections = parse_rustdoc_sections(typ.doc.trim());
let summary = sections.summary.trim();
let (title, description) = if summary.is_empty() {
(typ.name.clone(), String::new())
} else {
let mut parts = summary.splitn(2, '\n');
let raw_title = parts.next().unwrap_or("").trim().trim_end_matches('.');
let title = title_case_first(raw_title);
let description = parts.next().map(str::trim).unwrap_or("").to_string();
(title, description)
};
block.push_str("#' ");
block.push_str(&title);
block.push('\n');
if !description.is_empty() {
block.push_str("#'\n");
for line in description.lines() {
let line = line.trim_end();
if line.is_empty() {
block.push_str("#'\n");
} else {
block.push_str("#' ");
block.push_str(line);
block.push('\n');
}
}
}
for field in &typ.fields {
if field.binding_excluded {
continue;
}
let rname = field.name.trim_start_matches('_');
block.push_str("#' @field ");
block.push_str(rname);
block.push(' ');
block.push_str(&r_field_one_liner(rname, &field.doc));
block.push('\n');
}
block.push_str("#' @export\n");
block
}
pub(super) fn r_enum_roxygen_block(enum_def: &EnumDef, include_variants_as_fields: bool) -> String {
let mut block = String::with_capacity(256);
let sections = parse_rustdoc_sections(enum_def.doc.trim());
let summary = sections.summary.trim();
let (title, description) = if summary.is_empty() {
(enum_def.name.clone(), String::new())
} else {
let mut parts = summary.splitn(2, '\n');
let raw_title = parts.next().unwrap_or("").trim().trim_end_matches('.');
let title = title_case_first(raw_title);
let description = parts.next().map(str::trim).unwrap_or("").to_string();
(title, description)
};
block.push_str("#' ");
block.push_str(&title);
block.push('\n');
if !description.is_empty() {
block.push_str("#'\n");
for line in description.lines() {
let line = line.trim_end();
if line.is_empty() {
block.push_str("#'\n");
} else {
block.push_str("#' ");
block.push_str(line);
block.push('\n');
}
}
}
if include_variants_as_fields {
for variant in &enum_def.variants {
block.push_str("#' @field ");
block.push_str(&variant.name);
block.push(' ');
block.push_str(&r_field_one_liner(&variant.name, &variant.doc));
block.push('\n');
}
}
block.push_str("#' @export\n");
block
}
pub(super) fn gen_extendr_wrappers_r(
api: &ApiSurface,
package_name: &str,
input_type_names: &ahash::AHashSet<String>,
trait_bridge_fns: &[TraitBridgeFn],
r_exclude_functions: &ahash::AHashSet<String>,
bridges: &[TraitBridgeConfig],
) -> String {
let mut out = String::with_capacity(8 * 1024);
out.push_str("# Generated by extendr: Do not edit by hand\n");
out.push_str("#\n");
out.push_str("# This file is regenerated by alef on every `alef generate` run.\n");
out.push_str("# It mirrors the output of `rextendr::document()` and binds every\n");
out.push_str("# wrap__<symbol> entry registered in extendr_module! to an R-callable\n");
out.push_str("# function or class env.\n\n");
out.push_str(&crate::backends::extendr::template_env::render(
"r_use_dyn_lib.jinja",
minijinja::context! { package_name => package_name },
));
out.push_str("NULL\n\n");
let bridge_fn_names: ahash::AHashSet<&str> = trait_bridge_fns.iter().map(|tb| tb.name.as_str()).collect();
for func in &api.functions {
if bridge_fn_names.contains(func.name.as_str()) {
continue;
}
if r_exclude_functions.contains(&func.name) {
continue;
}
let params: Vec<String> = func.params.iter().map(|p| sanitize_r_param_name(&p.name)).collect();
let params_sig = r_wrapper_params_signature(&func.params, api);
let mut call_args = vec![format!("\"wrap__{}\"", func.name)];
for p in ¶ms {
call_args.push(p.clone());
}
call_args.push(format!("PACKAGE = \"{package_name}\""));
let call_args_str = call_args.join(", ");
let roxygen_block = r_roxygen_block(&func.name, &func.doc, &func.params, &func.return_type);
out.push_str(&crate::backends::extendr::template_env::render(
"r_free_function_wrapper.jinja",
minijinja::context! {
func_name => &func.name,
params_sig => params_sig,
call_args_str => call_args_str,
roxygen_block => roxygen_block,
},
));
}
for bridge_fn in trait_bridge_fns {
let params_sig = bridge_fn.params.join(", ");
let mut call_args = vec![format!("\"wrap__{}\"", bridge_fn.name)];
for p in &bridge_fn.params {
call_args.push(p.clone());
}
call_args.push(format!("PACKAGE = \"{package_name}\""));
let call_args_str = call_args.join(", ");
let kind = if bridge_fn.name.starts_with("register_") {
"register"
} else if bridge_fn.name.starts_with("unregister_") {
"unregister"
} else if bridge_fn.name.starts_with("clear_") {
"clear"
} else {
""
};
let roxygen_block = crate::backends::extendr::template_env::render(
"r_trait_bridge_roxygen.jinja",
minijinja::context! {
name => &bridge_fn.name,
kind => kind,
},
);
out.push_str(&crate::backends::extendr::template_env::render(
"r_free_function_wrapper.jinja",
minijinja::context! {
func_name => &bridge_fn.name,
params_sig => params_sig,
call_args_str => call_args_str,
roxygen_block => roxygen_block,
},
));
}
let s3_pairs = collect_s3_methods(api, trait_bridge_fns, bridges);
let s3_pairs_by_type: ahash::AHashMap<String, Vec<String>> = {
let mut map: ahash::AHashMap<String, Vec<String>> = ahash::AHashMap::new();
for (method_name, type_name) in &s3_pairs {
map.entry(type_name.clone()).or_default().push(method_name.clone());
}
map
};
let excluded = collect_excluded_class_types(api, bridges);
for typ in &api.types {
if typ.is_trait || excluded.contains(&typ.name) {
continue;
}
let class_roxygen = r_class_roxygen_block(typ);
out.push_str(&crate::backends::extendr::template_env::render(
"r_type_class_env.jinja",
minijinja::context! {
type_name => &typ.name,
roxygen_block => class_roxygen,
},
));
for method in &typ.methods {
if method_is_excluded_from_impl(method, api, bridges) {
continue;
}
let params: Vec<String> = method.params.iter().map(|p| sanitize_r_param_name(&p.name)).collect();
let params_sig = if method.is_static {
params.join(", ")
} else if params.is_empty() {
"self".to_string()
} else {
format!("self, {}", params.join(", "))
};
let mut call_args = vec![format!(
"\"wrap__{type_name}__{method_name}\"",
type_name = typ.name,
method_name = method.name,
)];
if !method.is_static {
call_args.push("self".to_string());
}
for p in ¶ms {
call_args.push(p.clone());
}
call_args.push(format!("PACKAGE = \"{package_name}\""));
let call_args_str = call_args.join(", ");
out.push_str(&crate::backends::extendr::template_env::render(
"r_method_binding.jinja",
minijinja::context! {
type_name => &typ.name,
method_name => &method.name,
params_sig => params_sig,
call_args_str => call_args_str,
},
));
}
if typ.has_default && !typ.fields.is_empty() && input_type_names.contains(&typ.name) {
out.push_str(&crate::backends::extendr::template_env::render(
"r_from_json_factory.jinja",
minijinja::context! {
type_name => &typ.name,
package_name => package_name,
},
));
}
out.push_str(&crate::backends::extendr::template_env::render(
"r_dollar_dispatch.jinja",
minijinja::context! { type_name => &typ.name },
));
out.push_str(&crate::backends::extendr::template_env::render(
"r_bracket_dispatch.jinja",
minijinja::context! { type_name => &typ.name },
));
if let Some(method_names) = s3_pairs_by_type.get(&typ.name) {
for method_name in method_names {
out.push_str(&crate::backends::extendr::template_env::render(
"r_s3_method.jinja",
minijinja::context! { name => method_name, type_name => &typ.name },
));
}
}
}
for e in &api.enums {
if !is_flat_data_enum(e) {
continue;
}
let type_name = &e.name;
let enum_roxygen = r_enum_roxygen_block(e, true);
out.push_str(&crate::backends::extendr::template_env::render(
"r_type_class_env.jinja",
minijinja::context! {
type_name => type_name,
roxygen_block => enum_roxygen,
},
));
out.push_str(&crate::backends::extendr::template_env::render(
"r_dollar_dispatch.jinja",
minijinja::context! { type_name => type_name },
));
out.push_str(&crate::backends::extendr::template_env::render(
"r_bracket_dispatch.jinja",
minijinja::context! { type_name => type_name },
));
}
for e in &api.enums {
if is_flat_data_enum(e) || is_json_passthrough_data_enum(e) {
continue;
}
let is_unit_enum = e.variants.iter().all(|v| v.fields.is_empty());
if !is_unit_enum {
continue;
}
let enum_name = &e.name;
out.push_str(&crate::backends::extendr::template_env::render(
"r_unit_enum_wrapper.jinja",
minijinja::context! { enum_name => enum_name },
));
}
for e in &api.enums {
if !is_json_passthrough_data_enum(e) {
continue;
}
let type_name = &e.name;
let enum_roxygen = r_enum_roxygen_block(e, false);
out.push_str(&crate::backends::extendr::template_env::render(
"r_type_class_env.jinja",
minijinja::context! {
type_name => type_name,
roxygen_block => enum_roxygen,
},
));
for method_name in ["default", "from_json"] {
let params_sig = if method_name == "from_json" { "json" } else { "" };
let mut call_args = vec![format!("\"wrap__{type_name}__{method_name}\"")];
if method_name == "from_json" {
call_args.push("json".to_string());
}
call_args.push(format!("PACKAGE = \"{package_name}\""));
let call_args_str = call_args.join(", ");
out.push_str(&crate::backends::extendr::template_env::render(
"r_method_binding.jinja",
minijinja::context! {
type_name => type_name,
method_name => method_name,
params_sig => params_sig,
call_args_str => call_args_str,
},
));
}
out.push_str(&crate::backends::extendr::template_env::render(
"r_dollar_dispatch.jinja",
minijinja::context! { type_name => type_name },
));
out.push_str(&crate::backends::extendr::template_env::render(
"r_bracket_dispatch.jinja",
minijinja::context! { type_name => type_name },
));
}
for generic_name in unique_s3_generic_names(&s3_pairs) {
out.push_str(&crate::backends::extendr::template_env::render(
"r_s3_generic.jinja",
minijinja::context! { name => generic_name },
));
}
out
}
pub(super) fn sanitize_r_param_name(name: &str) -> String {
name.trim_start_matches('_').to_string()
}
pub(super) fn r_wrapper_params_signature(params: &[ParamDef], api: &ApiSurface) -> String {
let default_types: ahash::AHashSet<&str> = api
.types
.iter()
.filter(|t| t.has_default)
.map(|t| t.name.as_str())
.collect();
params
.iter()
.map(|p| {
let sanitized_name = sanitize_r_param_name(&p.name);
if let TypeRef::Named(name) = &p.ty
&& default_types.contains(name.as_str())
{
format!("{} = {}$default()", sanitized_name, name)
} else if p.optional || matches!(p.ty, TypeRef::Optional(_)) {
format!("{} = NULL", sanitized_name)
} else {
sanitized_name
}
})
.collect::<Vec<_>>()
.join(", ")
}
pub(super) fn collect_s3_methods(
api: &ApiSurface,
trait_bridge_fns: &[TraitBridgeFn],
bridges: &[TraitBridgeConfig],
) -> Vec<(String, String)> {
let excluded_types = collect_excluded_class_types(api, bridges);
let mut reserved: ahash::AHashSet<String> = api.functions.iter().map(|f| f.name.clone()).collect();
for bridge_fn in trait_bridge_fns {
reserved.insert(bridge_fn.name.clone());
}
let mut pairs: Vec<(String, String)> = Vec::new();
for typ in &api.types {
if typ.is_trait || excluded_types.contains(&typ.name) {
continue;
}
for method in &typ.methods {
if method.is_static || method_is_excluded_from_impl(method, api, bridges) {
continue;
}
if reserved.contains(&method.name) {
continue;
}
pairs.push((method.name.clone(), typ.name.clone()));
}
}
pairs
}
pub(super) fn unique_s3_generic_names(pairs: &[(String, String)]) -> Vec<String> {
let mut names: Vec<String> = pairs.iter().map(|(name, _)| name.clone()).collect();
names.sort();
names.dedup();
names
}
pub(super) fn gen_namespace(
api: &ApiSurface,
package_name: &str,
trait_bridge_fns: &[TraitBridgeFn],
r_exclude_functions: &ahash::AHashSet<String>,
bridges: &[TraitBridgeConfig],
) -> String {
let mut out = String::with_capacity(2 * 1024);
out.push_str("# Generated by alef — do not edit.\n\n");
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_use_dyn_lib.jinja",
minijinja::context! { package_name => package_name },
));
out.push('\n');
let bridge_fn_names: ahash::AHashSet<&str> = trait_bridge_fns.iter().map(|tb| tb.name.as_str()).collect();
for func in &api.functions {
if bridge_fn_names.contains(func.name.as_str()) {
continue;
}
if r_exclude_functions.contains(&func.name) {
continue;
}
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_export.jinja",
minijinja::context! { name => &func.name },
));
}
for bridge_fn in trait_bridge_fns {
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_export.jinja",
minijinja::context! { name => &bridge_fn.name },
));
}
if find_r_options_type_from_api(api).is_some() {
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_export.jinja",
minijinja::context! { name => "conversion_options" },
));
}
let excluded = collect_excluded_class_types(api, bridges);
for typ in &api.types {
if typ.is_trait || excluded.contains(&typ.name) {
continue;
}
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_export.jinja",
minijinja::context! { name => &typ.name },
));
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_s3method.jinja",
minijinja::context! { method_type => "$", name => &typ.name },
));
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_s3method.jinja",
minijinja::context! { method_type => "[[", name => &typ.name },
));
}
for e in &api.enums {
if !is_flat_data_enum(e) {
continue;
}
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_export.jinja",
minijinja::context! { name => &e.name },
));
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_s3method.jinja",
minijinja::context! { method_type => "$", name => &e.name },
));
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_s3method.jinja",
minijinja::context! { method_type => "[[", name => &e.name },
));
}
for e in &api.enums {
if !is_json_passthrough_data_enum(e) {
continue;
}
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_export.jinja",
minijinja::context! { name => &e.name },
));
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_s3method.jinja",
minijinja::context! { method_type => "$", name => &e.name },
));
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_s3method.jinja",
minijinja::context! { method_type => "[[", name => &e.name },
));
}
let s3_pairs = collect_s3_methods(api, trait_bridge_fns, bridges);
for generic_name in unique_s3_generic_names(&s3_pairs) {
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_export.jinja",
minijinja::context! { name => &generic_name },
));
}
for (method_name, type_name) in &s3_pairs {
out.push_str(&crate::backends::extendr::template_env::render(
"r_namespace_s3method_named.jinja",
minijinja::context! { method_name => method_name, type_name => type_name },
));
}
out
}