alef 0.24.7

Opinionated polyglot binding generator for Rust libraries
Documentation
use crate::backends::ffi::type_map::is_void_return;
use crate::core::ir::{ParamDef, TypeRef};
use ahash::AHashSet;
use minijinja::context;

use super::super::helpers::null_return_value;
use super::signatures::c_symbol_component;

fn type_ref_to_rust_type(ty: &TypeRef, core_import: &str) -> String {
    match ty {
        TypeRef::String | TypeRef::Char => "String".to_string(),
        TypeRef::Bytes => "Vec<u8>".to_string(),
        TypeRef::Primitive(prim) => match prim {
            crate::core::ir::PrimitiveType::Bool => "bool".to_string(),
            crate::core::ir::PrimitiveType::U8 => "u8".to_string(),
            crate::core::ir::PrimitiveType::U16 => "u16".to_string(),
            crate::core::ir::PrimitiveType::U32 => "u32".to_string(),
            crate::core::ir::PrimitiveType::U64 => "u64".to_string(),
            crate::core::ir::PrimitiveType::I8 => "i8".to_string(),
            crate::core::ir::PrimitiveType::I16 => "i16".to_string(),
            crate::core::ir::PrimitiveType::I32 => "i32".to_string(),
            crate::core::ir::PrimitiveType::I64 => "i64".to_string(),
            crate::core::ir::PrimitiveType::F32 => "f32".to_string(),
            crate::core::ir::PrimitiveType::F64 => "f64".to_string(),
            crate::core::ir::PrimitiveType::Usize => "usize".to_string(),
            crate::core::ir::PrimitiveType::Isize => "isize".to_string(),
        },
        TypeRef::Named(name) => format!("{core_import}::{name}"),
        TypeRef::Vec(inner) => format!("Vec<{}>", type_ref_to_rust_type(inner, core_import)),
        TypeRef::Map(key, val) => format!(
            "std::collections::HashMap<{}, {}>",
            type_ref_to_rust_type(key, core_import),
            type_ref_to_rust_type(val, core_import)
        ),
        TypeRef::Optional(inner) => format!("Option<{}>", type_ref_to_rust_type(inner, core_import)),
        TypeRef::Path => "std::path::PathBuf".to_string(),
        TypeRef::Json => "serde_json::Value".to_string(),
        TypeRef::Duration => "std::time::Duration".to_string(),
        TypeRef::Unit => "()".to_string(),
    }
}

// ---------------------------------------------------------------------------
// Parameter conversion (C types -> Rust)
// ---------------------------------------------------------------------------

pub(super) fn gen_param_conversion_with_enums(
    param: &ParamDef,
    has_error: bool,
    is_bytes_result: bool,
    return_type: &TypeRef,
    core_import: &str,
    enum_names: &AHashSet<String>,
) -> String {
    let name = &param.name;
    let rs_name = format!("{name}_rs");
    let mut out = String::with_capacity(2048);

    let fail_ret = if is_bytes_result || (has_error && is_void_return(return_type)) {
        "return -1;"
    } else if is_void_return(return_type) {
        "return;"
    } else {
        // Use null_return_value to get the correct default for the return type
        // (handles primitives, floats, Optional, Duration, pointers)
        match null_return_value(return_type) {
            "()" => "return;",
            v => {
                // Leak: we need a 'static str but null_return_value returns &'static str
                // The values are all string literals so this is fine
                let ret = format!("return {};", v);
                // Use a leaked string since fail_ret needs 'static lifetime
                // This is called once per function generation, not in a hot loop
                Box::leak(ret.into_boxed_str()) as &str
            }
        }
    };

    if param.optional {
        // Optional parameter — null means None
        match &param.ty {
            TypeRef::String | TypeRef::Char => {
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_optional_string_conversion.jinja",
                    context! {
                        rs_name => rs_name.clone(),
                        name => name.clone(),
                        fail_ret => fail_ret.to_string(),
                    },
                ));
            }
            TypeRef::Path => {
                out.push(' ');
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_path_conversion.jinja",
                    context! {
                        rs_name => rs_name.clone(),
                        name => name.clone(),
                        is_ref => param.is_ref,
                        fail_ret => fail_ret.to_string(),
                    },
                ));
            }
            TypeRef::Json => {
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_optional_json_conversion.jinja",
                    context! {
                        rs_name => rs_name.clone(),
                        name => name.clone(),
                        fail_ret => fail_ret.to_string(),
                        turbofish => String::new(),
                    },
                ));
            }
            TypeRef::Named(type_name) if enum_names.contains(type_name.as_str()) => {
                // Optional enum passed as i32 sentinel: reconstruct via private Rust helper.
                // Use match+explicit return rather than `?` because the outer function may return
                // *mut T or i32, not Result/Option, so the ? operator is unavailable.
                //
                // IMPORTANT: the local variable name is `{rs_name}` (derived from the FFI param
                // name), NOT from `enum_snake` (which is derived from the type name). The
                // conversion helper is named `{enum_snake}_from_i32_rs` and receives `{name}`
                // (the actual FFI param), not `{enum_snake}` which may differ when param name
                // != snake_case(type_name) (e.g. param `strategy` of type `RedactionStrategy`).
                let enum_snake = c_symbol_component(type_name);
                out.push_str(&crate::backends::ffi::template_env::render(
                    "ffi_enum_discriminant_match.jinja",
                    context! {
                        rs_name => rs_name.clone(),
                        enum_snake => enum_snake,
                        name => name.clone(),
                        error_message => format!("invalid enum discriminant for {type_name}"),
                        fail_ret => fail_ret,
                    },
                ));
            }
            TypeRef::Named(_type_name) => {
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_optional_named_conversion.jinja",
                    context! {
                        rs_name => rs_name.clone(),
                        name => name.clone(),
                        is_ref => param.is_ref,
                    },
                ));
            }
            TypeRef::Primitive(crate::core::ir::PrimitiveType::Bool) => {
                out.push(' ');
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_optional_bool_conversion.jinja",
                    context! {
                        rs_name => rs_name.clone(),
                        name => name.clone(),
                    },
                ));
            }
            TypeRef::Primitive(prim) => {
                // Optional numeric primitive: max value of type = None
                let max_val = match prim {
                    crate::core::ir::PrimitiveType::U8 => "u8::MAX",
                    crate::core::ir::PrimitiveType::U16 => "u16::MAX",
                    crate::core::ir::PrimitiveType::U32 => "u32::MAX",
                    crate::core::ir::PrimitiveType::U64 => "u64::MAX",
                    crate::core::ir::PrimitiveType::I8 => "i8::MAX",
                    crate::core::ir::PrimitiveType::I16 => "i16::MAX",
                    crate::core::ir::PrimitiveType::I32 => "i32::MAX",
                    crate::core::ir::PrimitiveType::I64 => "i64::MAX",
                    crate::core::ir::PrimitiveType::F32 => "f32::NAN",
                    crate::core::ir::PrimitiveType::F64 => "f64::NAN",
                    crate::core::ir::PrimitiveType::Usize => "usize::MAX",
                    crate::core::ir::PrimitiveType::Isize => "isize::MAX",
                    crate::core::ir::PrimitiveType::Bool => unreachable!("handled above"),
                };
                let is_float = matches!(
                    prim,
                    crate::core::ir::PrimitiveType::F32 | crate::core::ir::PrimitiveType::F64
                );
                out.push(' ');
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_optional_numeric_conversion.jinja",
                    context! {
                        rs_name => rs_name.clone(),
                        name => name.clone(),
                        max_val => max_val,
                        is_float => is_float,
                    },
                ));
            }
            TypeRef::Vec(_) | TypeRef::Map(_, _) => {
                // Optional Vec/Map: deserialize from JSON string
                let type_hint = match &param.ty {
                    TypeRef::Vec(_) => {
                        format!("::<{}>", type_ref_to_rust_type(&param.ty, core_import))
                    }
                    TypeRef::Map(_, val_ty) if param.map_is_ahash => {
                        // AHashMap target: deserialize directly into AHashMap with the correct
                        // key type so no post-deserialization conversion is needed.
                        let val_rust = type_ref_to_rust_type(val_ty, core_import);
                        let key_rust = if param.map_key_is_cow {
                            "std::borrow::Cow<'static, str>".to_string()
                        } else {
                            "String".to_string()
                        };
                        format!("::<ahash::AHashMap<{key_rust}, {val_rust}>>")
                    }
                    TypeRef::Map(_, _) => {
                        format!("::<{}>", type_ref_to_rust_type(&param.ty, core_import))
                    }
                    _ => String::new(),
                };
                out.push(' ');
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_optional_vec_map_conversion.jinja",
                    context! {
                        rs_name => rs_name.clone(),
                        name => name.clone(),
                        turbofish => type_hint,
                        fail_ret => fail_ret.to_string(),
                    },
                ));
            }
            _ => {
                // Fallback: treat as nullable JSON string
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_optional_fallback.jinja",
                    context! {
                        rs_name => rs_name.clone(),
                        name => name.clone(),
                        fail_ret => fail_ret.to_string(),
                    },
                ));
            }
        }
    } else {
        match &param.ty {
            TypeRef::String | TypeRef::Char => {
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_non_optional_string_conversion.jinja",
                    context! {
                        name => name.clone(),
                        fail_ret => fail_ret.to_string(),
                        rs_name => rs_name.clone(),
                    },
                ));
            }
            TypeRef::Path => {
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_non_optional_path_conversion.jinja",
                    context! {
                        rs_name => rs_name.clone(),
                        name => name.clone(),
                        fail_ret => fail_ret.to_string(),
                    },
                ));
            }
            TypeRef::Json => {
                let turbofish = String::new();
                let mut_keyword = String::new();
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_non_optional_json_conversion.jinja",
                    context! {
                        name => name.clone(),
                        fail_ret => fail_ret.to_string(),
                        rs_name => rs_name.clone(),
                        turbofish => turbofish,
                        mut_keyword => mut_keyword,
                    },
                ));
            }
            TypeRef::Primitive(prim) => match prim {
                crate::core::ir::PrimitiveType::Bool => {
                    out.push_str(&crate::backends::ffi::template_env::render(
                        "param_primitive_bool.jinja",
                        context! { rs_name => rs_name.clone(), name => name.clone() },
                    ));
                }
                _ => {
                    if let Some(newtype_path) = &param.newtype_wrapper {
                        // Param was resolved from a newtype (e.g. NodeIndex→u32): re-wrap for core call.
                        out.push_str(&crate::backends::ffi::template_env::render("param_primitive_newtype.jinja", context! { rs_name => rs_name.clone(), newtype_path => newtype_path.clone(), name => name.clone() }));
                    } else {
                        out.push_str(&crate::backends::ffi::template_env::render(
                            "param_primitive_passthrough.jinja",
                            context! { rs_name => rs_name.clone(), name => name.clone() },
                        ));
                    }
                }
            },
            TypeRef::Named(type_name) if enum_names.contains(type_name.as_str()) => {
                // Enum passed as i32: reconstruct using the private Rust helper.
                // Use match+explicit return rather than `?` because the outer function may return
                // *mut T or i32, not Result/Option, so the ? operator is unavailable.
                //
                // IMPORTANT: the local variable name is `{rs_name}` (derived from the FFI param
                // name, e.g. `strategy_rs`), NOT `{enum_snake}_rs` (which would use the type
                // name, e.g. `redaction_strategy_rs`). The conversion helper is still named
                // `{enum_snake}_from_i32_rs` but it receives `{name}` (the actual FFI param).
                // This fixes the mismatch when param name != snake_case(type_name).
                let enum_snake = c_symbol_component(type_name);
                out.push_str(&crate::backends::ffi::template_env::render(
                    "ffi_enum_discriminant_match.jinja",
                    context! {
                        rs_name => rs_name.clone(),
                        enum_snake => enum_snake,
                        name => name.clone(),
                        error_message => format!("invalid enum discriminant for {type_name}"),
                        fail_ret => fail_ret,
                    },
                ));
            }
            TypeRef::Named(_type_name) => {
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_non_optional_named_conversion.jinja",
                    context! {
                        rs_name => rs_name.clone(),
                        name => name.clone(),
                        fail_ret => fail_ret.to_string(),
                        is_ref => param.is_ref,
                        is_mut => param.is_mut,
                    },
                ));
            }
            TypeRef::Bytes => {
                // Bytes come as (*const u8, len: usize) — the len param is a separate
                // parameter named {name}_len by convention. A null pointer is allowed
                // when the corresponding length is zero (empty input is a legitimate
                // case — e.g. extracting from a 0-byte file). Reject null only when
                // the caller claims a non-zero length.
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_non_optional_bytes_conversion.jinja",
                    context! {
                        rs_name => rs_name.clone(),
                        name => name.clone(),
                        fail_ret => fail_ret.to_string(),
                    },
                ));
            }
            TypeRef::Vec(_) | TypeRef::Map(_, _) => {
                // Passed as JSON string
                let mut_keyword = if param.is_mut { "mut " } else { "" };
                let type_hint = match &param.ty {
                    TypeRef::Vec(_) => {
                        format!("::<{}>", type_ref_to_rust_type(&param.ty, core_import))
                    }
                    TypeRef::Map(_, val_ty) if param.map_is_ahash => {
                        // AHashMap target: deserialize directly.
                        let val_rust = type_ref_to_rust_type(val_ty, core_import);
                        let key_rust = if param.map_key_is_cow {
                            "std::borrow::Cow<'static, str>".to_string()
                        } else {
                            "String".to_string()
                        };
                        format!("::<ahash::AHashMap<{key_rust}, {val_rust}>>")
                    }
                    TypeRef::Map(_, _) => {
                        format!("::<{}>", type_ref_to_rust_type(&param.ty, core_import))
                    }
                    _ => String::new(),
                };
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_non_optional_json_conversion.jinja",
                    context! {
                        name => name.clone(),
                        fail_ret => fail_ret.to_string(),
                        rs_name => rs_name.clone(),
                        turbofish => type_hint,
                        mut_keyword => mut_keyword,
                    },
                ));
            }
            TypeRef::Optional(_) => {
                // Should not happen for non-optional param, but handle gracefully
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_optional_passthrough.jinja",
                    context! { rs_name => rs_name.clone(), name => name.clone() },
                ));
            }
            TypeRef::Duration => {
                // Duration passed as u64 milliseconds
                out.push_str(&crate::backends::ffi::template_env::render(
                    "param_duration_conversion.jinja",
                    context! { rs_name => rs_name.clone(), name => name.clone() },
                ));
            }
            TypeRef::Unit => {
                // No parameter to convert
            }
        }
    }

    out
}