alef-backend-kotlin 0.17.35

Kotlin (JVM) backend for alef
Documentation
//! Kotlin/Native binding generator — Phase 3.
//!
//! Emits Kotlin/Native source that calls into the cbindgen-produced C FFI library
//! via `kotlinx.cinterop.*`. The consumer pattern mirrors the Go and Zig backends:
//! use `config.ffi_prefix()`, `config.ffi_header_name()`, and `config.ffi_lib_name()`
//! as the single source of truth for symbol names and linking directives.

mod cinterop_def;
mod native_build_gradle;
mod native_types;

use alef_codegen::c_consumer;
use alef_core::backend::GeneratedFile;
use alef_core::config::ResolvedCrateConfig;
use alef_core::ir::{ApiSurface, FunctionDef, ParamDef, TypeRef};
use std::collections::BTreeSet;
use std::path::PathBuf;

use crate::gen_bindings::{to_lower_camel, to_pascal_case};

/// Emit all Kotlin/Native files for the given API surface.
///
/// Returns three generated files:
/// 1. `packages/kotlin-native/src/nativeMain/kotlin/<package>/<Module>.kt`
/// 2. `packages/kotlin-native/<crate>.def`
/// 3. `packages/kotlin-native/build.gradle.kts`
pub fn emit(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
    let kt = emit_kotlin_source(api, config);
    let def = cinterop_def::emit_def_file(config);
    let gradle = native_build_gradle::emit_gradle_build(config);

    let package = config.kotlin_package();
    let package_path = package.replace('.', "/");
    let module_name = to_pascal_case(&config.name);
    let crate_name = &config.name;

    let native_root = "packages/kotlin-native".to_string();

    let kt_path = PathBuf::from(&native_root)
        .join("src/nativeMain/kotlin")
        .join(&package_path)
        .join(format!("{module_name}.kt"));

    let def_path = PathBuf::from(&native_root).join(format!("{crate_name}.def"));
    let gradle_path = PathBuf::from(&native_root).join("build.gradle.kts");

    Ok(vec![
        GeneratedFile {
            path: kt_path,
            content: kt,
            generated_header: false,
        },
        GeneratedFile {
            path: def_path,
            content: def,
            generated_header: false,
        },
        GeneratedFile {
            path: gradle_path,
            content: gradle,
            generated_header: false,
        },
    ])
}

// ---------------------------------------------------------------------------
// Kotlin/Native source file
// ---------------------------------------------------------------------------

fn emit_kotlin_source(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
    let package = config.kotlin_package();
    let module_name = to_pascal_case(&config.name);
    let prefix = config.ffi_prefix();
    let crate_name = &config.name;

    let exclude_functions: std::collections::HashSet<&str> = config
        .kotlin
        .as_ref()
        .map(|c| c.exclude_functions.iter().map(String::as_str).collect())
        .unwrap_or_default();
    let exclude_types: std::collections::HashSet<&str> = config
        .kotlin
        .as_ref()
        .map(|c| c.exclude_types.iter().map(String::as_str).collect())
        .unwrap_or_default();

    let mut imports: BTreeSet<String> = BTreeSet::new();
    imports.insert("import kotlinx.cinterop.*".to_string());
    imports.insert(format!("import {crate_name}.*"));

    let mut body = String::new();

    for ty in api.types.iter().filter(|t| !exclude_types.contains(t.name.as_str())) {
        native_types::emit_native_type(ty, &mut body);
        body.push('\n');
    }

    for en in api.enums.iter().filter(|e| !exclude_types.contains(e.name.as_str())) {
        native_types::emit_native_enum(en, &mut body);
        body.push('\n');
    }

    for error in &api.errors {
        native_types::emit_native_error(error, &mut body);
        body.push('\n');
    }

    let visible_functions: Vec<&alef_core::ir::FunctionDef> = api
        .functions
        .iter()
        .filter(|f| !exclude_functions.contains(f.name.as_str()))
        .collect();

    if !visible_functions.is_empty() {
        body.push_str(&crate::template_env::render(
            "object_declaration.jinja",
            minijinja::context! {
                name => module_name,
            },
        ));
        for f in &visible_functions {
            emit_native_function(f, &prefix, &mut body);
            body.push('\n');
        }
        body.push_str("}\n");
    }

    let mut content = String::new();
    content.push_str("// Generated by alef. Do not edit by hand.\n\n");
    content.push_str(&crate::template_env::render(
        "package_declaration.jinja",
        minijinja::context! {
            package => package,
        },
    ));
    content.push_str("\n\n");
    for import in &imports {
        content.push_str(import);
        content.push('\n');
    }
    content.push('\n');
    content.push_str(&body);
    content
}

/// Emit a Kotlin/Native function body — exposed for `gen_mpp` to reuse.
pub(crate) fn emit_native_function_pub(f: &FunctionDef, prefix: &str, out: &mut String) {
    emit_native_function(f, prefix, out)
}

fn emit_native_function(f: &FunctionDef, prefix: &str, out: &mut String) {
    if !f.doc.is_empty() {
        let doc_lines: Vec<String> = f.doc.lines().map(ToString::to_string).collect();
        out.push_str(&crate::template_env::render(
            "doc_comment.jinja",
            minijinja::context! {
                indent => "    ",
                lines => doc_lines,
            },
        ));
    }

    let params: Vec<String> = f.params.iter().map(format_native_param).collect();
    let return_ty = native_return_type_str(&f.return_type);
    let func_name_camel = to_lower_camel(&f.name);

    let error_code_sym = c_consumer::last_error_code_symbol(prefix);
    let error_context_sym = c_consumer::last_error_context_symbol(prefix);
    let free_sym = c_consumer::free_string_symbol(prefix);
    let c_fn = format!("{prefix}_{}", f.name);

    out.push_str(&crate::template_env::render(
        "native_function_header.jinja",
        minijinja::context! {
            name => func_name_camel,
            params => params.join(", "),
            return_type => return_ty,
        },
    ));
    out.push_str("        return memScoped {\n");

    // Emit C-string conversions for string/path parameters inside the memScoped block.
    for p in &f.params {
        emit_native_param_conversion(p, out);
    }

    // Build the C call argument list.
    let c_args: Vec<String> = f.params.iter().map(native_c_arg).collect();
    let call = format!("{c_fn}({})", c_args.join(", "));

    if f.error_type.is_some() {
        // Fallible: capture result, check error code, throw if non-zero.
        out.push_str(&crate::template_env::render(
            "native_result_assign.jinja",
            minijinja::context! {
                call => call,
            },
        ));
        out.push_str(&crate::template_env::render(
            "native_error_code_check.jinja",
            minijinja::context! {
                error_code_sym => error_code_sym,
            },
        ));
        out.push_str("            if (_code != 0) {\n");
        out.push_str(&crate::template_env::render(
            "native_error_message.jinja",
            minijinja::context! {
                error_context_sym => error_context_sym,
            },
        ));
        out.push_str("                throw RuntimeException(_msg)\n");
        out.push_str("            }\n");
        if matches!(f.return_type, TypeRef::Unit) {
            out.push_str("            Unit\n");
        } else {
            let expr = native_unwrap_return("_result", &f.return_type, &free_sym);
            out.push_str(&crate::template_env::render(
                "native_return_expr.jinja",
                minijinja::context! {
                    expr => expr,
                },
            ));
        }
    } else if matches!(f.return_type, TypeRef::Unit) {
        out.push_str(&crate::template_env::render(
            "native_call_only.jinja",
            minijinja::context! {
                call => call,
            },
        ));
        out.push_str("            Unit\n");
    } else {
        out.push_str(&crate::template_env::render(
            "native_result_assign.jinja",
            minijinja::context! {
                call => call,
            },
        ));
        let expr = native_unwrap_return("_result", &f.return_type, &free_sym);
        out.push_str(&crate::template_env::render(
            "native_return_expr.jinja",
            minijinja::context! {
                expr => expr,
            },
        ));
    }

    out.push_str("        }\n");
    out.push_str("    }\n");
}

fn format_native_param(p: &ParamDef) -> String {
    let ty_str = native_param_type_str(&p.ty, p.optional);
    format!("{}: {}", to_lower_camel(&p.name), ty_str)
}

/// The Kotlin/Native type used at the wrapper boundary for a function parameter.
///
/// `String`, `Path`, `Bytes`, `Vec`, `Map` → `String` (caller supplies value;
/// String/Path are converted to `cstr`, Bytes/Vec/Map are passed as JSON strings).
fn native_param_type_str(ty: &TypeRef, optional: bool) -> String {
    let inner = match ty {
        TypeRef::String | TypeRef::Path | TypeRef::Char | TypeRef::Json | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
            "String".to_string()
        }
        TypeRef::Bytes => "ByteArray".to_string(),
        TypeRef::Optional(inner) => return format!("{}?", native_param_type_str(inner, false)),
        other => native_type_str(other, false),
    };
    if optional { format!("{inner}?") } else { inner }
}

/// Emit the `memScoped`-local conversion for a parameter before the C call.
fn emit_native_param_conversion(p: &ParamDef, out: &mut String) {
    let name = to_lower_camel(&p.name);
    match &p.ty {
        TypeRef::String | TypeRef::Path | TypeRef::Char | TypeRef::Json | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
            out.push_str(&crate::template_env::render(
                "native_param_cstr_conversion.jinja",
                minijinja::context! {
                    name => &name,
                },
            ));
        }
        TypeRef::Bytes => {
            // Bytes: pin the ByteArray and pass pointer + length.
            out.push_str(&crate::template_env::render(
                "native_param_bytes_conversion.jinja",
                minijinja::context! {
                    name => &name,
                },
            ));
        }
        _ => {}
    }
}

/// The C argument expression for a parameter.
fn native_c_arg(p: &ParamDef) -> String {
    let name = to_lower_camel(&p.name);
    match &p.ty {
        TypeRef::String | TypeRef::Path | TypeRef::Char | TypeRef::Json | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
            format!("{name}C")
        }
        TypeRef::Bytes => format!("{name}Pin.addressOf(0), {name}.size"),
        _ => name,
    }
}

/// Produce the Kotlin expression that converts a raw C return value to the
/// Kotlin return type.
///
/// String-like returns: copy via `toKString()` then free the C allocation.
/// Everything else: pass through unchanged.
fn native_unwrap_return(raw: &str, ty: &TypeRef, free_sym: &str) -> String {
    match ty {
        TypeRef::String | TypeRef::Path | TypeRef::Char | TypeRef::Json | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
            format!("run {{ val _s = {raw}!!.toKString(); {free_sym}({raw}); _s }}")
        }
        TypeRef::Bytes => {
            // Bytes: reconstruct a ByteArray from the returned pointer + length is not standard
            // in the simple C ABI; fall back to treating as a string for now.
            format!("run {{ val _s = {raw}!!.toKString().encodeToByteArray(); {free_sym}({raw}); _s }}")
        }
        _ => raw.to_string(),
    }
}

/// Kotlin/Native return type for a function (not for struct fields).
fn native_return_type_str(ty: &TypeRef) -> String {
    match ty {
        TypeRef::Unit => "Unit".to_string(),
        TypeRef::String | TypeRef::Path | TypeRef::Char | TypeRef::Json | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
            "String".to_string()
        }
        TypeRef::Bytes => "ByteArray".to_string(),
        other => native_type_str(other, false),
    }
}

/// Kotlin/Native type for struct fields (same as JVM type; cinterop types are
/// used only inside function bodies, not in data class declarations).
pub(crate) fn native_type_str(ty: &TypeRef, optional: bool) -> String {
    use alef_core::ir::PrimitiveType;
    let inner = match ty {
        TypeRef::Primitive(p) => match p {
            PrimitiveType::Bool => "Boolean".to_string(),
            PrimitiveType::U8 | PrimitiveType::I8 => "Byte".to_string(),
            PrimitiveType::U16 | PrimitiveType::I16 => "Short".to_string(),
            PrimitiveType::U32 | PrimitiveType::I32 => "Int".to_string(),
            PrimitiveType::U64 | PrimitiveType::I64 | PrimitiveType::Usize | PrimitiveType::Isize => "Long".to_string(),
            PrimitiveType::F32 => "Float".to_string(),
            PrimitiveType::F64 => "Double".to_string(),
        },
        TypeRef::String | TypeRef::Json => "String".to_string(),
        // Path is just String in Native mode — no java.nio.file.Path.
        TypeRef::Path => "String".to_string(),
        // Char: single Unicode codepoint, represented as a Kotlin Char.
        TypeRef::Char => "Char".to_string(),
        TypeRef::Bytes => "ByteArray".to_string(),
        TypeRef::Unit => "Unit".to_string(),
        TypeRef::Duration => "Long".to_string(),
        TypeRef::Named(name) => name.clone(),
        TypeRef::Optional(inner) => return format!("{}?", native_type_str(inner, false)),
        TypeRef::Vec(inner) => format!("List<{}>", native_type_str(inner, false)),
        TypeRef::Map(k, v) => format!("Map<{}, {}>", native_type_str(k, false), native_type_str(v, false)),
    };
    if optional { format!("{inner}?") } else { inner }
}