use crate::core::hash::{self, CommentStyle};
use serde_json;
pub(crate) fn ffi_c_type_name(ffi_prefix: &str, rust_basename: &str) -> String {
let prefix_upper = ffi_prefix.to_uppercase();
format!("{prefix_upper}{rust_basename}")
}
struct CallbackSpec {
c_field: String,
export_name: String,
go_method: String,
doc: String,
extra: Vec<ExtraParam>,
has_is_header: bool,
}
struct ExtraParam {
c_name: String,
c_type: String,
go_name: String,
go_iface_type: String,
decode: String,
}
fn snake_to_lower_camel(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut next_upper = false;
for ch in s.chars() {
if ch == '_' {
next_upper = true;
} else if next_upper {
result.extend(ch.to_uppercase());
next_upper = false;
} else {
result.push(ch);
}
}
result
}
fn callback_specs_from_trait(trait_def: &crate::core::ir::TypeDef) -> Vec<CallbackSpec> {
use crate::core::ir::{PrimitiveType, TypeRef};
use heck::ToPascalCase;
let mut specs = Vec::with_capacity(trait_def.methods.len());
'methods: for m in &trait_def.methods {
if m.trait_source.is_some() {
continue;
}
let go_method = m.name.to_pascal_case();
let export_name = format!("go{go_method}");
let first_line = m.doc.lines().next().unwrap_or("").trim().to_string();
let doc = if first_line.is_empty() {
format!("{go_method} visits this element.")
} else {
format!("{go_method} {}", first_line.to_lowercase().trim_end_matches('.'))
};
let mut extra = Vec::new();
let mut has_is_header = false;
for p in &m.params {
if matches!(&p.ty, TypeRef::Named(_)) {
continue;
}
let param_name = p.name.trim_start_matches('_').to_string();
let go_param_name = snake_to_lower_camel(¶m_name);
match (&p.ty, p.optional) {
(TypeRef::String, false) => {
let decode = format!("C.GoString({param_name})");
extra.push(ExtraParam {
c_name: param_name,
c_type: "*C.char".to_string(),
go_name: go_param_name,
go_iface_type: "string".to_string(),
decode,
});
}
(TypeRef::String, true) => {
let decode = format!("optGoString({param_name})");
extra.push(ExtraParam {
c_name: param_name,
c_type: "*C.char".to_string(),
go_name: go_param_name,
go_iface_type: "*string".to_string(),
decode,
});
}
(TypeRef::Primitive(PrimitiveType::Bool), false) => {
let decode = format!("{param_name} != 0");
extra.push(ExtraParam {
c_name: param_name.clone(),
c_type: "C.int32_t".to_string(),
go_name: go_param_name.clone(),
go_iface_type: "bool".to_string(),
decode,
});
}
(TypeRef::Primitive(PrimitiveType::U32 | PrimitiveType::I32), false) => {
let decode = format!("uint32({param_name})");
extra.push(ExtraParam {
c_name: param_name.clone(),
c_type: "C.uint32_t".to_string(),
go_name: go_param_name.clone(),
go_iface_type: "uint32".to_string(),
decode,
});
}
(TypeRef::Primitive(PrimitiveType::Usize | PrimitiveType::U64), false) => {
let decode = format!("uint({param_name})");
extra.push(ExtraParam {
c_name: param_name.clone(),
c_type: "C.uintptr_t".to_string(),
go_name: go_param_name.clone(),
go_iface_type: "uint".to_string(),
decode,
});
}
(TypeRef::Vec(inner), false) => match inner.as_ref() {
TypeRef::String => {
let decode = format!("decodeCellsJSON({param_name})");
extra.push(ExtraParam {
c_name: param_name.clone(),
c_type: "*C.char".to_string(),
go_name: go_param_name.clone(),
go_iface_type: "[]string".to_string(),
decode,
});
has_is_header = true;
break;
}
_ => {
eprintln!(
"[alef] gen_visitor(go): skip method `{}` — unsupported Vec param `{}`",
m.name, p.name
);
continue 'methods;
}
},
_ => {
eprintln!(
"[alef] gen_visitor(go): skip method `{}` — unsupported param `{}: {:?}`",
m.name, p.name, p.ty
);
continue 'methods;
}
}
}
specs.push(CallbackSpec {
c_field: m.name.clone(),
export_name,
go_method,
doc,
extra,
has_is_header,
});
}
specs
}
#[allow(clippy::too_many_arguments)]
pub fn gen_visitor_file(
pkg_name: &str,
ffi_prefix: &str,
ffi_header: &str,
ffi_crate_dir: &str,
to_root: &str,
vtable_trait_name: &str,
options_field: &str,
trait_def: &crate::core::ir::TypeDef,
) -> String {
let specs = callback_specs_from_trait(trait_def);
let mut out = String::with_capacity(32_768);
out.push_str(&hash::header(CommentStyle::DoubleSlash));
let pascal_prefix = {
let mut chars = ffi_prefix.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
};
let prefix_upper = ffi_prefix.to_uppercase();
let vtable_rust_name = format!("{pascal_prefix}{vtable_trait_name}VTable");
let bridge_rust_name = format!("{pascal_prefix}{vtable_trait_name}Bridge");
let vtable_c_type = ffi_c_type_name(ffi_prefix, &vtable_rust_name);
let bridge_c_type = ffi_c_type_name(ffi_prefix, &bridge_rust_name);
let conversion_options_type = format!("{prefix_upper}ConversionOptions");
let bridge_snake = to_snake_case(&bridge_rust_name);
let fn_bridge_new = format!("{ffi_prefix}_{bridge_snake}_new");
let fn_bridge_free = format!("{ffi_prefix}_{bridge_snake}_free");
let fn_options_set_visitor = format!("{ffi_prefix}_options_set_{options_field}");
let fn_options_free = format!("{ffi_prefix}_conversion_options_free");
let fn_options_from_json = format!("{ffi_prefix}_conversion_options_from_json");
let fn_convert = format!("{ffi_prefix}_convert");
let fn_result_free = format!("{ffi_prefix}_conversion_result_free");
let callbacks: Vec<_> = specs
.iter()
.map(|spec| {
minijinja::Value::from_serialize(serde_json::json!({
"export_name": spec.export_name,
"c_sig": c_signature(spec),
"c_field": spec.c_field,
}))
})
.collect();
out.push_str(&crate::backends::go::template_env::render(
"visitor_preamble.jinja",
minijinja::context! {
pkg_name => pkg_name,
to_root => to_root,
ffi_crate_dir => ffi_crate_dir,
ffi_header => ffi_header,
callbacks => callbacks,
vtable_c_type => vtable_c_type.clone(),
},
));
out.push_str(&crate::backends::go::template_env::render(
"visitor_node_context_and_result.jinja",
minijinja::Value::default(),
));
out.push('\n');
out.push_str(&crate::backends::go::template_env::render(
"visitor_interface_header.jinja",
minijinja::Value::default(),
));
for spec in &specs {
let param_str = iface_param_str(spec);
out.push_str(&crate::backends::go::template_env::render(
"visitor_interface_method.jinja",
minijinja::context! {
doc => spec.doc,
method => spec.go_method,
params => param_str,
},
));
}
out.push_str(&crate::backends::go::template_env::render(
"visitor_interface_footer.jinja",
minijinja::Value::default(),
));
out.push('\n');
out.push_str(&crate::backends::go::template_env::render(
"base_visitor_header.jinja",
minijinja::Value::default(),
));
out.push('\n');
for spec in &specs {
let param_str = iface_param_str(spec);
let blank_ids: Vec<String> = iface_param_names(spec).into_iter().collect();
out.push_str(&crate::backends::go::template_env::render(
"base_visitor_method.jinja",
minijinja::context! {
doc => spec.go_method,
method_name => spec.go_method,
params => param_str,
blank_ids => blank_ids,
},
));
out.push('\n');
}
out.push_str(&crate::backends::go::template_env::render(
"visitor_registry_block.jinja",
minijinja::Value::default(),
));
out.push('\n');
out.push_str(&crate::backends::go::template_env::render(
"decode_node_context.jinja",
minijinja::Value::default(),
));
out.push('\n');
out.push_str(&crate::backends::go::template_env::render(
"encode_visit_result.jinja",
minijinja::Value::default(),
));
out.push('\n');
out.push_str(&crate::backends::go::template_env::render(
"opt_go_string.jinja",
minijinja::Value::default(),
));
out.push('\n');
out.push_str(&crate::backends::go::template_env::render(
"decode_cells_json.jinja",
minijinja::Value::default(),
));
out.push('\n');
for spec in &specs {
gen_trampoline(&mut out, spec);
}
let fn_result_to_json = fn_result_free.replace("_free", "_to_json");
out.push_str(&crate::backends::go::template_env::render(
"convert_with_visitor_helper.jinja",
minijinja::context! {
conversion_options_type => conversion_options_type,
fn_options_from_json => fn_options_from_json,
fn_options_free => fn_options_free,
fn_bridge_new => fn_bridge_new,
fn_bridge_free => fn_bridge_free,
fn_options_set_visitor => fn_options_set_visitor,
bridge_c_type => bridge_c_type,
fn_convert => fn_convert,
fn_result_to_json => fn_result_to_json,
fn_result_free => fn_result_free,
},
));
out.push('\n');
out.push('\n');
out
}
fn c_signature(spec: &CallbackSpec) -> String {
let mut parts = vec!["void* user_data".to_string(), "char* ctx".to_string()];
for ep in &spec.extra {
let ctype = match ep.c_type.as_str() {
"*C.char" => "char*",
"C.int32_t" => "int32_t",
"C.uint32_t" => "uint32_t",
"C.uintptr_t" => "uintptr_t",
_ => "void*",
};
parts.push(format!("{ctype} {}", ep.c_name));
}
if spec.has_is_header {
parts.push("int32_t isHeader".to_string());
}
parts.push("char** out_result".to_string());
parts.join(", ")
}
fn iface_param_str(spec: &CallbackSpec) -> String {
let mut params = vec!["ctx NodeContext".to_string()];
for ep in &spec.extra {
params.push(format!("{} {}", ep.go_name, ep.go_iface_type));
}
if spec.has_is_header {
params.push("isHeader bool".to_string());
}
params.join(", ")
}
fn iface_param_names(spec: &CallbackSpec) -> Vec<String> {
let mut names = vec!["ctx".to_string()];
for ep in &spec.extra {
names.push(ep.go_name.clone());
}
if spec.has_is_header {
names.push("isHeader".to_string());
}
names
}
fn gen_trampoline(out: &mut String, spec: &CallbackSpec) {
let mut go_params = vec!["userData unsafe.Pointer".to_string(), "ctx *C.char".to_string()];
for ep in &spec.extra {
go_params.push(format!("{} {}", ep.c_name, ep.c_type));
}
if spec.has_is_header {
go_params.push("isHeader C.int32_t".to_string());
}
go_params.push("outResult **C.char".to_string());
out.push_str(&crate::backends::go::template_env::render(
"export_marker.jinja",
minijinja::context! {
name => spec.export_name,
},
));
out.push_str(&crate::backends::go::template_env::render(
"trampoline_func_signature.jinja",
minijinja::context! {
name => spec.export_name,
params => go_params.join(", "),
},
));
out.push_str(&crate::backends::go::template_env::render(
"trampoline_lookup.jinja",
minijinja::Value::default(),
));
for ep in &spec.extra {
out.push_str(&crate::backends::go::template_env::render(
"trampoline_param_decode.jinja",
minijinja::context! {
name => capitalize(&ep.go_name),
decode => ep.decode,
},
));
}
if spec.has_is_header {
out.push_str(&crate::backends::go::template_env::render(
"trampoline_is_header.jinja",
minijinja::Value::default(),
));
}
let mut call_args = vec!["nodeCtx".to_string()];
for ep in &spec.extra {
call_args.push(format!("go{}", capitalize(&ep.go_name)));
}
if spec.has_is_header {
call_args.push("goIsHeader".to_string());
}
out.push_str(&crate::backends::go::template_env::render(
"trampoline_func_call.jinja",
minijinja::context! {
method => spec.go_method,
args => call_args.join(", "),
},
));
out.push_str("}\n");
out.push('\n');
}
#[allow(clippy::too_many_arguments, dead_code)]
fn gen_convert_with_visitor(
out: &mut String,
_ffi_prefix: &str,
_conversion_options_type: &str,
_vtable_c_type: &str,
_bridge_c_type: &str,
_fn_bridge_new: &str,
_fn_bridge_free: &str,
_fn_options_set_visitor: &str,
_fn_options_free: &str,
_fn_options_from_json: &str,
_fn_convert: &str,
_fn_result_free: &str,
) {
out.push_str(&crate::backends::go::template_env::render(
"convert_with_visitor_wrapper.jinja",
minijinja::Value::default(),
));
out.push('\n');
}
fn capitalize(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
fn to_snake_case(s: &str) -> String {
let mut out = String::new();
for (i, ch) in s.chars().enumerate() {
if ch.is_ascii_uppercase() && i > 0 {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
}
out
}