use crate::core::ir::{FunctionDef, ParamDef, TypeRef};
use super::conversions::{dart_call_arg, frb_rust_type_inner, primitive_name};
use super::helpers::emit_cleaned_dartdoc;
pub(crate) fn emit_bridge_fn(
out: &mut String,
f: &FunctionDef,
source_crate_name: &str,
type_paths: &std::collections::HashMap<String, String>,
types_needing_from_conversion: &std::collections::HashSet<String>,
opaque_type_names: &std::collections::HashSet<String>,
_stub_methods: &[String],
) {
emit_cleaned_dartdoc(out, &f.doc, "");
let fn_name = &f.name;
let async_kw = if f.is_async { "async " } else { "" };
let params: Vec<String> = f
.params
.iter()
.map(|p| {
let rust_ty = frb_rust_type_mirror(&p.ty, p.optional);
let is_opaque_handle = if let TypeRef::Named(type_name) = &p.ty {
opaque_type_names.contains(type_name.as_str())
} else {
false
};
let mut_prefix = if p.is_mut && is_opaque_handle { "mut " } else { "" };
format!("{mut_prefix}{}: {rust_ty}", p.name)
})
.collect();
let has_error = f.error_type.is_some();
let return_ty = if has_error {
format!("Result<{}, String>", frb_rust_type_mirror(&f.return_type, false))
} else {
frb_rust_type_mirror(&f.return_type, false)
};
out.push_str(&crate::backends::dart::template_env::render(
"rust_bridge_fn_open.jinja",
minijinja::context! {
async_kw => async_kw,
fn_name => fn_name.as_str(),
params => params.join(", "),
return_ty => return_ty.as_str(),
},
));
let resolved_path = if f.rust_path.is_empty() {
format!("{source_crate_name}::{fn_name}")
} else {
f.rust_path.replace('-', "_")
};
if f.return_sanitized {
let suppress = if f.params.is_empty() {
String::new()
} else {
let names: Vec<&str> = f.params.iter().map(|p| p.name.as_str()).collect();
if names.len() == 1 {
format!(" let _ = {};\n", names[0])
} else {
format!(" let _ = ({});\n", names.join(", "))
}
};
let default_value = sanitized_return_default(&f.return_type);
let body = if has_error {
format!("{suppress} Ok({default_value})\n")
} else {
format!("{suppress} {default_value}\n")
};
out.push_str(&body);
out.push_str("}\n");
return;
}
let mut pre_call_bindings: Vec<String> = Vec::new();
let call_args: Vec<String> = f
.params
.iter()
.map(|p| {
if let TypeRef::Map(_, _) = &p.ty {
if p.map_is_ahash && p.map_key_is_cow {
let bound_name = format!("__{}_ahash", p.name);
pre_call_bindings.push(format!(
" let {bound_name} = {}.map(|m| m.into_iter().map(|(k, v)| (std::borrow::Cow::Owned(k), serde_json::Value::String(v))).collect::<ahash::AHashMap<std::borrow::Cow<'static, str>, serde_json::Value>>());",
p.name
));
return if p.optional && p.is_ref {
format!("{bound_name}.as_ref()")
} else if p.is_ref {
format!("{bound_name}.as_ref().unwrap()")
} else {
bound_name
};
}
}
dart_call_arg_with_mirror_transmute(
p,
source_crate_name,
type_paths,
types_needing_from_conversion,
opaque_type_names,
)
})
.collect();
let call = format!("{resolved_path}({})", call_args.join(", "));
let ret_transmute = return_transmute_expr(
&f.return_type,
source_crate_name,
type_paths,
opaque_type_names,
f.returns_ref,
);
let result_cast = if ret_transmute.is_empty() {
build_primitive_result_cast(&f.return_type, f.returns_ref)
} else {
String::new()
};
let body = build_body(&call, &result_cast, &ret_transmute, has_error, f.is_async);
if !pre_call_bindings.is_empty() {
for binding in &pre_call_bindings {
out.push_str(binding);
out.push('\n');
}
}
out.push_str(&body);
out.push_str("}\n");
}
pub(crate) fn frb_rust_type_mirror(ty: &TypeRef, optional: bool) -> String {
let inner = frb_rust_type_mirror_inner(ty);
if optional { format!("Option<{inner}>") } else { inner }
}
fn frb_rust_type_mirror_inner(ty: &TypeRef) -> String {
match ty {
TypeRef::Primitive(p) => frb_rust_type_inner(&TypeRef::Primitive(p.clone())),
TypeRef::String | TypeRef::Char => "String".to_string(),
TypeRef::Bytes => "Vec<u8>".to_string(),
TypeRef::Optional(inner) => format!("Option<{}>", frb_rust_type_mirror_inner(inner)),
TypeRef::Vec(inner) => format!("Vec<{}>", frb_rust_type_mirror_inner(inner)),
TypeRef::Map(k, v) => format!(
"std::collections::HashMap<{}, {}>",
frb_rust_type_mirror_inner(k),
frb_rust_type_mirror_inner(v)
),
TypeRef::Named(name) => name.clone(),
TypeRef::Path => "String".to_string(),
TypeRef::Unit => "()".to_string(),
TypeRef::Json => "String".to_string(),
TypeRef::Duration => "i64".to_string(),
}
}
fn dart_call_arg_with_mirror_transmute(
p: &ParamDef,
source_crate_name: &str,
type_paths: &std::collections::HashMap<String, String>,
types_needing_from_conversion: &std::collections::HashSet<String>,
opaque_type_names: &std::collections::HashSet<String>,
) -> String {
let name = &p.name;
let original = p.original_type.as_deref().unwrap_or("");
let stripped_orig = original
.trim()
.trim_start_matches('&')
.trim_start_matches("mut ")
.trim();
if !stripped_orig.is_empty() && stripped_orig.starts_with("Vec(") && stripped_orig.contains("Named(\"(") {
let tuple_inner = stripped_orig
.find("Named(\"(")
.and_then(|start| {
let rest = &stripped_orig[start + 8..];
rest.find(")\")")
.map(|end| rest[..end].trim_end_matches(')').to_string())
})
.unwrap_or_default();
if tuple_inner.starts_with("PathBuf,") || tuple_inner.starts_with("PathBuf ,") {
return format!("{name}.into_iter().map(|p| (std::path::PathBuf::from(p), None)).collect::<Vec<_>>()");
}
if tuple_inner.starts_with("Vec<u8>,") || tuple_inner.starts_with("Vec<u8> ,") {
return format!(
"{{ let _ = {name}; compile_error!(\"alef cannot bridge Vec<(Vec<u8>, ...)> through Dart FRB; configure dart.exclude_functions for this item\") }}"
);
}
}
if matches!(p.ty, TypeRef::Path) {
if p.optional {
if p.is_ref {
return format!("{name}.as_ref().map(std::path::Path::new)");
}
return format!("{name}.map(std::path::PathBuf::from)");
}
if p.is_ref {
return format!("std::path::Path::new(&{name})");
}
return format!("std::path::PathBuf::from({name})");
}
if let TypeRef::Primitive(prim) = &p.ty {
let target = primitive_name(prim);
if target != "i64" && target != "f64" && target != "bool" {
if p.optional {
return format!("{name}.map(|v| v as {target})");
}
return if p.is_ref {
format!("&({name} as {target})")
} else {
format!("{name} as {target}")
};
}
}
if let TypeRef::Vec(inner) = &p.ty {
if let TypeRef::Primitive(prim) = inner.as_ref() {
let target = primitive_name(prim);
if target != "i64" && target != "f64" && target != "bool" {
if p.optional {
if p.is_ref {
return format!(
"{name}.as_ref().map(|v| v.iter().map(|x| *x as {target}).collect::<Vec<_>>()).as_deref()"
);
}
return format!("{name}.map(|v| v.into_iter().map(|x| x as {target}).collect::<Vec<_>>())");
}
if p.is_ref {
return format!("{name}.iter().map(|x| *x as {target}).collect::<Vec<_>>().as_slice()");
}
return format!("{name}.into_iter().map(|x| x as {target}).collect::<Vec<_>>()");
}
}
}
if let TypeRef::Named(type_name) = &p.ty {
if opaque_type_names.contains(type_name.as_str()) {
if p.optional {
if p.is_ref {
return format!("{name}.as_ref().map(|h| &h.inner)");
}
return format!("{name}.map(|h| h.inner)");
}
if p.is_mut {
return format!("&mut {name}.inner");
}
if p.is_ref {
return format!("&{name}.inner");
}
return format!("{name}.inner");
}
}
if let TypeRef::Named(type_name) = &p.ty {
let core_ty = resolve_core_type(type_name, source_crate_name, type_paths);
if types_needing_from_conversion.contains(type_name.as_str()) {
return build_named_in_from(name, type_name, &core_ty, p.is_ref, p.is_mut, p.optional);
}
return build_named_in_transmute(name, type_name, &core_ty, p.is_ref, p.is_mut, p.optional);
}
if let TypeRef::Vec(inner) = &p.ty {
if let TypeRef::Named(type_name) = inner.as_ref() {
let core_ty = resolve_core_type(type_name, source_crate_name, type_paths);
if types_needing_from_conversion.contains(type_name.as_str()) {
if p.optional {
return format!("{name}.map(|v| v.into_iter().map({core_ty}::from).collect::<Vec<_>>())");
}
return format!("{name}.into_iter().map({core_ty}::from).collect::<Vec<_>>()");
}
if p.optional {
return format!(
"{name}.map(|v| v.into_iter().map(|x| unsafe {{ std::mem::transmute::<{type_name}, {core_ty}>(x) }}).collect::<Vec<_>>())"
);
}
if p.is_ref {
return format!(
"unsafe {{ std::slice::from_raw_parts(\
std::mem::transmute::<*const {type_name}, *const {core_ty}>({name}.as_ptr()), \
{name}.len()) }}"
);
}
if p.is_mut {
return format!(
"unsafe {{ std::slice::from_raw_parts_mut(\
std::mem::transmute::<*mut {type_name}, *mut {core_ty}>({name}.as_mut_ptr()), \
{name}.len()) }}"
);
}
return format!(
"{name}.into_iter().map(|x| unsafe {{ std::mem::transmute::<{type_name}, {core_ty}>(x) }}).collect::<Vec<_>>()"
);
}
}
if let TypeRef::Optional(inner) = &p.ty {
if let TypeRef::Named(type_name) = inner.as_ref() {
let core_ty = resolve_core_type(type_name, source_crate_name, type_paths);
if types_needing_from_conversion.contains(type_name.as_str()) {
return format!("{name}.map({core_ty}::from)");
}
return format!("{name}.map(|v| unsafe {{ std::mem::transmute::<{type_name}, {core_ty}>(v) }})");
}
}
dart_call_arg(p)
}
fn resolve_core_type(
type_name: &str,
source_crate_name: &str,
type_paths: &std::collections::HashMap<String, String>,
) -> String {
match type_paths.get(type_name) {
Some(path) => path.clone(),
None => format!("{source_crate_name}::{type_name}"),
}
}
fn build_named_in_from(
name: &str,
_mirror_name: &str,
core_ty: &str,
is_ref: bool,
is_mut: bool,
optional: bool,
) -> String {
if optional {
return format!("{name}.map({core_ty}::from)");
}
if is_mut {
return format!("&mut {core_ty}::from({name})");
}
if is_ref {
return format!("&{core_ty}::from({name})");
}
format!("{core_ty}::from({name})")
}
fn build_named_in_transmute(
name: &str,
mirror_name: &str,
core_ty: &str,
is_ref: bool,
is_mut: bool,
optional: bool,
) -> String {
if optional {
return format!("{name}.map(|v| unsafe {{ std::mem::transmute::<{mirror_name}, {core_ty}>(v) }})");
}
if is_mut {
return format!("unsafe {{ std::mem::transmute::<&mut {mirror_name}, &mut {core_ty}>(&mut {name}) }}");
}
if is_ref {
return format!("unsafe {{ std::mem::transmute::<&{mirror_name}, &{core_ty}>(&{name}) }}");
}
format!("unsafe {{ std::mem::transmute::<{mirror_name}, {core_ty}>({name}) }}")
}
fn return_transmute_expr(
ty: &TypeRef,
_source_crate_name: &str,
_type_paths: &std::collections::HashMap<String, String>,
opaque_type_names: &std::collections::HashSet<String>,
returns_ref: bool,
) -> String {
match ty {
TypeRef::Named(mirror_name) => {
if opaque_type_names.contains(mirror_name.as_str()) {
format!("|inner| {mirror_name} {{ inner }}")
} else {
format!("{mirror_name}::from")
}
}
TypeRef::Vec(inner) => {
if let TypeRef::Named(mirror_name) = inner.as_ref() {
if returns_ref {
if opaque_type_names.contains(mirror_name.as_str()) {
format!(
"|v: &[_]| v.iter().map(|inner| {mirror_name} {{ inner: inner.clone() }}).collect::<Vec<_>>()"
)
} else {
format!("|v: &[_]| v.iter().map(|x| {mirror_name}::from(x.clone())).collect::<Vec<_>>()")
}
} else if opaque_type_names.contains(mirror_name.as_str()) {
format!("|v: Vec<_>| v.into_iter().map(|inner| {mirror_name} {{ inner }}).collect::<Vec<_>>()")
} else {
format!("|v: Vec<_>| v.into_iter().map({mirror_name}::from).collect::<Vec<_>>()")
}
} else {
String::new()
}
}
TypeRef::Optional(inner) => {
if let TypeRef::Named(mirror_name) = inner.as_ref() {
if opaque_type_names.contains(mirror_name.as_str()) {
format!("|v: Option<_>| v.map(|inner| {mirror_name} {{ inner }})")
} else {
format!("|v: Option<_>| v.map({mirror_name}::from)")
}
} else {
String::new()
}
}
_ => String::new(),
}
}
fn sanitized_return_default(ty: &TypeRef) -> String {
match ty {
TypeRef::Unit => "()".to_string(),
TypeRef::String | TypeRef::Char | TypeRef::Path => "String::new()".to_string(),
TypeRef::Bytes => "Vec::new()".to_string(),
TypeRef::Primitive(p) => match p {
crate::core::ir::PrimitiveType::Bool => "false".to_string(),
crate::core::ir::PrimitiveType::F32 => "0.0f32".to_string(),
crate::core::ir::PrimitiveType::F64 => "0.0f64".to_string(),
_ => "0".to_string(),
},
TypeRef::Optional(_) => "None".to_string(),
TypeRef::Vec(_) => "Vec::new()".to_string(),
TypeRef::Map(_, _) => "Default::default()".to_string(),
TypeRef::Duration => "0".to_string(),
TypeRef::Named(_) | TypeRef::Json => "Default::default()".to_string(),
}
}
fn build_primitive_result_cast(ty: &TypeRef, returns_ref: bool) -> String {
match ty {
TypeRef::Primitive(_) => {
let target = frb_rust_type_inner(ty);
format!(" as {target}")
}
TypeRef::Path => ".display().to_string()".to_string(),
TypeRef::String | TypeRef::Char => ".to_string()".to_string(),
TypeRef::Optional(inner)
if matches!(inner.as_ref(), TypeRef::String | TypeRef::Path | TypeRef::Char) && returns_ref =>
{
".map(|v| v.to_string())".to_string()
}
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::Primitive(prim) => {
let target = primitive_name(prim);
if target == "f64" || target == "i64" || target == "bool" {
String::new()
} else {
format!(
".into_iter().map(|x| x as {}).collect::<Vec<_>>()",
frb_rust_type_inner(inner)
)
}
}
TypeRef::Path => {
".into_iter().map(|p| p.to_string_lossy().into_owned()).collect::<Vec<_>>()".to_string()
}
TypeRef::String | TypeRef::Char => ".into_iter().map(|s| s.to_string()).collect::<Vec<_>>()".to_string(),
TypeRef::Vec(inner2) => {
if let TypeRef::Primitive(prim) = inner2.as_ref() {
let target = primitive_name(prim);
let frb_target = frb_rust_type_inner(inner2);
if target != frb_target.as_str() {
format!(
".into_iter().map(|row| row.into_iter().map(|x| x as {frb_target}).collect::<Vec<_>>()).collect::<Vec<_>>()"
)
} else {
String::new()
}
} else {
String::new()
}
}
_ => String::new(),
},
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::String | TypeRef::Path | TypeRef::Char => ".map(|s| s.to_string())".to_string(),
_ => String::new(),
},
_ => String::new(),
}
}
fn build_body(call: &str, result_cast: &str, ret_transmute: &str, has_error: bool, is_async: bool) -> String {
if !ret_transmute.is_empty() {
let transmute_fn = ret_transmute;
if has_error {
if is_async {
return format!(" {call}.await.map({transmute_fn}).map_err(|e| e.to_string())\n");
}
return format!(" {call}.map({transmute_fn}).map_err(|e| e.to_string())\n");
}
if is_async {
return format!(" ({transmute_fn})({call}.await)\n");
}
return format!(" ({transmute_fn})({call})\n");
}
if has_error {
if is_async {
return format!(" {call}.await.map(|v| v{result_cast}).map_err(|e| e.to_string())\n");
}
return format!(" {call}.map(|v| v{result_cast}).map_err(|e| e.to_string())\n");
}
if is_async {
if result_cast.is_empty() {
return format!(" {call}.await\n");
}
return format!(" {call}.await{result_cast}\n");
}
if result_cast.is_empty() {
format!(" {call}\n")
} else {
format!(" {call}{result_cast}\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_param(name: &str, ty: TypeRef, is_ref: bool, is_mut: bool, optional: bool) -> ParamDef {
ParamDef {
name: name.to_string(),
ty,
optional,
default: None,
sanitized: false,
typed_default: None,
is_ref,
is_mut,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
}
}
#[test]
fn is_mut_named_opaque_emits_mut_inner() {
let p = make_param(
"result",
TypeRef::Named("ExtractionResult".to_string()),
false,
true,
false,
);
let mut opaque: std::collections::HashSet<String> = std::collections::HashSet::new();
opaque.insert("ExtractionResult".to_string());
let needs_from: std::collections::HashSet<String> = std::collections::HashSet::new();
let type_paths: std::collections::HashMap<String, String> = std::collections::HashMap::new();
let expr = dart_call_arg_with_mirror_transmute(&p, "mylib", &type_paths, &needs_from, &opaque);
assert_eq!(expr, "&mut result.inner", "is_mut opaque param must use &mut: {expr}");
}
#[test]
fn is_mut_named_from_emits_mut_borrow() {
let p = make_param(
"cfg",
TypeRef::Named("TranslationConfig".to_string()),
false,
true,
false,
);
let opaque: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut needs_from: std::collections::HashSet<String> = std::collections::HashSet::new();
needs_from.insert("TranslationConfig".to_string());
let type_paths: std::collections::HashMap<String, String> = std::collections::HashMap::new();
let expr = dart_call_arg_with_mirror_transmute(&p, "mylib", &type_paths, &needs_from, &opaque);
assert!(
expr.contains("&mut"),
"is_mut From-converted Named param must emit &mut borrow: {expr}"
);
}
#[test]
fn is_mut_named_transmute_emits_mut_transmute() {
let p = make_param("config", TypeRef::Named("MyConfig".to_string()), false, true, false);
let opaque: std::collections::HashSet<String> = std::collections::HashSet::new();
let needs_from: std::collections::HashSet<String> = std::collections::HashSet::new();
let type_paths: std::collections::HashMap<String, String> = std::collections::HashMap::new();
let expr = dart_call_arg_with_mirror_transmute(&p, "mylib", &type_paths, &needs_from, &opaque);
assert!(
expr.contains("&mut"),
"is_mut transmute Named param must emit &mut transmute: {expr}"
);
assert!(
expr.contains("transmute"),
"is_mut transmute Named param must emit transmute: {expr}"
);
}
#[test]
fn vec_named_is_ref_emits_slice_not_raw_pointer() {
let p = make_param(
"categories",
TypeRef::Vec(Box::new(TypeRef::Named("PiiCategory".to_string()))),
true,
false,
false,
);
let opaque: std::collections::HashSet<String> = std::collections::HashSet::new();
let needs_from: std::collections::HashSet<String> = std::collections::HashSet::new();
let type_paths: std::collections::HashMap<String, String> = std::collections::HashMap::new();
let expr = dart_call_arg_with_mirror_transmute(&p, "mylib", &type_paths, &needs_from, &opaque);
assert!(
expr.contains("from_raw_parts"),
"Vec<Named> is_ref must use slice::from_raw_parts, got: {expr}"
);
assert!(
expr.contains(".len()"),
"Vec<Named> is_ref must include .len() for slice bounds, got: {expr}"
);
}
#[test]
fn collect_in_return_transmute_vec_has_type_annotation() {
let opaque: std::collections::HashSet<String> = std::collections::HashSet::new();
let ty = TypeRef::Vec(Box::new(TypeRef::Named("QrCode".to_string())));
let expr = return_transmute_expr(&ty, "mylib", &std::collections::HashMap::new(), &opaque, false);
assert!(
expr.contains("collect::<Vec<_>>()"),
"Vec<Named> collect must have type annotation: {expr}"
);
}
#[test]
fn path_vec_result_cast_uses_to_string_lossy() {
let ty = TypeRef::Vec(Box::new(TypeRef::Path));
let cast = build_primitive_result_cast(&ty, false);
assert!(
cast.contains("to_string_lossy"),
"Vec<Path> cast must use to_string_lossy: {cast}"
);
assert!(
!cast.contains(".to_string()"),
"Vec<Path> must NOT use .to_string(): {cast}"
);
}
#[test]
fn scalar_path_result_cast_uses_display_not_to_string() {
let ty = TypeRef::Path;
let cast = build_primitive_result_cast(&ty, false);
assert!(cast.contains("display()"), "Path cast must use .display(): {cast}");
assert!(cast.contains("to_string()"), "Path cast must use to_string(): {cast}");
}
}