alef 0.79.3

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Helper functions for Zig code generation.
//!
//! Provides utilities for FFI introspection and documentation emission.

use crate::codegen::c_consumer;
use crate::core::config::Language;
use crate::core::ir::ErrorDef;
use crate::docs::clean_doc;

/// Emit the two standard helpers every generated file needs:
///
/// - `_free_string`: wraps the C `{prefix}_free_string` symbol to release
///   FFI-allocated strings. Caller must NOT use the pointer after this call.
/// - `_last_error`: reads the thread-local last-error state set by the FFI
///   layer. Returns a Zig slice pointing into thread-local storage; the
///   pointer is valid until the next FFI call.
///
/// `declared_errors` is the list of error sets declared in the module (in
/// declaration order). `_error_with_message` dispatches on the stable numeric
/// FFI taxonomy code carried by `#[alef(error_code = N)]`. A failure that no
/// declared code substantiates maps to `error.UnknownFfiError` — the member
/// `emit_error_set` injects into every generated error set — mirroring how the
/// C FFI layer surfaces `ALEF_FFI_UNKNOWN_ERROR`. It must never resolve to an
/// arbitrary declared variant, which would report a specific, wrong error.
pub(crate) fn emit_helpers(prefix: &str, declared_errors: &[ErrorDef], out: &mut String) {
    let free_symbol = c_consumer::free_string_symbol(prefix);
    let error_code_symbol = c_consumer::last_error_code_symbol(prefix);
    let error_context_symbol = c_consumer::last_error_context_symbol(prefix);

    out.push_str("/// Free a string allocated by the FFI layer.\n");
    out.push_str(&crate::backends::zig::template_env::render(
        "helper_free_string_doc1.jinja",
        minijinja::context! {
            prefix => prefix,
        },
    ));
    out.push_str("/// Do NOT call this twice on the same pointer.\n");
    out.push_str("pub fn _free_string(ptr: [*c]u8) void {\n");
    out.push_str(&crate::backends::zig::template_env::render(
        "helper_free_string_doc2.jinja",
        minijinja::context! {
            free_symbol => free_symbol,
        },
    ));
    out.push_str("}\n\n");

    out.push_str("/// Retrieve the last error set by the FFI layer, if any.\n");
    out.push_str("/// Returns a slice into thread-local storage valid until the next FFI call.\n");
    out.push_str("pub fn _last_error() ?[]const u8 {\n");
    out.push_str(&crate::backends::zig::template_env::render(
        "helper_last_error_code.jinja",
        minijinja::context! {
            symbol => error_code_symbol,
        },
    ));
    out.push_str("    if (_code == 0) return null;\n");
    out.push_str(&crate::backends::zig::template_env::render(
        "helper_last_error_ctx.jinja",
        minijinja::context! {
            symbol => error_context_symbol,
        },
    ));
    out.push_str("    if (_ctx == null) return null;\n");
    out.push_str("    return std.mem.sliceTo(_ctx, 0);\n");
    out.push_str("}\n\n");

    let dispatching: Vec<&ErrorDef> = declared_errors
        .iter()
        .filter(|error| error.variants.iter().any(|variant| variant.error_code.is_some()))
        .collect();

    out.push_str("/// Map the last FFI error to a typed error.\n");
    if dispatching.is_empty() {
        out.push_str("/// No declared variant carries a stable numeric FFI taxonomy code\n");
        out.push_str("/// (`#[alef(error_code = N)]`), so no specific variant can be\n");
        out.push_str("/// substantiated: every failure maps to `error.UnknownFfiError`.\n");
    } else {
        out.push_str("/// Dispatches exclusively on the stable numeric FFI taxonomy code.\n");
        out.push_str("/// A code matching no declared variant maps to `error.UnknownFfiError`.\n");
    }
    out.push_str("inline fn _error_with_message(comptime E: type) E {\n");
    out.push_str("    _ = _last_error();\n");
    if !dispatching.is_empty() {
        out.push_str(&format!(
            "    const code = @as(i32, @intCast(c.{error_code_symbol}()));\n"
        ));
        for error in dispatching {
            out.push_str(&format!("    if (E == {}) return switch (code) {{\n", error.name));
            for variant in &error.variants {
                let Some(error_code) = variant.error_code else {
                    continue;
                };
                let variant_name = crate::codegen::naming::public_host_identifier(
                    Language::Zig,
                    crate::codegen::naming::PublicIdentifierKind::Type,
                    &variant.name,
                );
                out.push_str(&format!("        {error_code} => error.{variant_name},\n"));
            }
            out.push_str("        else => error.UnknownFfiError,\n    };\n");
        }
    }
    out.push_str("    return error.UnknownFfiError;\n");
    out.push_str("}\n");
}

/// Emit cleaned Zig documentation for a declaration.
///
/// Cleans Rust-specific doc strings and formats as Zig doc comments (/// ...).
pub(crate) fn emit_cleaned_zig_doc(out: &mut String, doc: &str, indent: &str) {
    if doc.is_empty() {
        return;
    }
    let cleaned = clean_doc(doc, Language::Zig);
    crate::codegen::doc_emission::emit_zig_doc(out, &cleaned, indent);
}

#[cfg(test)]
mod tests {
    use super::*;

    fn request_error(error_code: Option<u32>) -> ErrorDef {
        ErrorDef {
            name: "RequestError".to_string(),
            rust_path: "sample::RequestError".to_string(),
            variants: vec![crate::core::ir::ErrorVariant {
                error_code,
                name: "InvalidInput".to_string(),
                is_unit: true,
                ..Default::default()
            }],
            original_rust_path: String::new(),
            doc: String::new(),
            methods: Vec::new(),
            binding_excluded: false,
            binding_exclusion_reason: None,
            version: Default::default(),
        }
    }

    #[test]
    fn error_with_message_dispatches_to_each_declared_error() {
        let errors = vec![request_error(Some(100))];
        let mut out = String::new();
        emit_helpers("example_pack", &errors, &mut out);

        assert!(
            out.contains("100 => error.InvalidInput"),
            "missing numeric taxonomy dispatch:\n{out}"
        );
        assert!(
            !out.contains("std.debug.print"),
            "FFI helpers must not write to stderr:\n{out}"
        );
        assert!(
            out.contains("        else => error.UnknownFfiError,\n"),
            "a code matching no declared variant must resolve to the opaque unknown error:\n{out}"
        );
        assert!(
            out.contains("    return error.UnknownFfiError;\n"),
            "an error set with no dispatch block must resolve to the opaque unknown error:\n{out}"
        );
    }

    /// A variant with no `#[alef(error_code = N)]` cannot be substantiated from the FFI
    /// taxonomy code, so it must not appear in the switch — and, critically, the arm it does
    /// not fill must not be back-filled with some other declared variant. Zig used to emit
    /// `_first_error(E)` here, which returns `@field(E, fields[0].name)` — the *first declared*
    /// variant — so every failure was reported as a specific, wrong error rather than an
    /// unknown one. See `ALEF_FFI_UNKNOWN_ERROR` in the C layer for the honest equivalent. ~keep
    #[test]
    fn unnumbered_error_variants_never_resolve_to_a_declared_variant() {
        let errors = vec![request_error(None)];
        let mut out = String::new();

        emit_helpers("example_pack", &errors, &mut out);

        assert!(
            !out.contains("error.InvalidInput"),
            "an uncoded variant must never be named by the dispatcher:\n{out}"
        );
        assert!(
            !out.contains("_first_error"),
            "the first-declared-variant fallback must not be emitted at all:\n{out}"
        );
        assert!(
            !out.contains("fields[0].name"),
            "nothing may resolve an FFI failure by declaration order:\n{out}"
        );
        assert!(
            !out.contains("switch (code)"),
            "with no coded variant there is nothing to dispatch on:\n{out}"
        );
        assert!(
            out.contains("    return error.UnknownFfiError;\n"),
            "every failure must surface as the opaque unknown error:\n{out}"
        );
        assert!(
            !out.contains("Dispatches exclusively on the stable numeric FFI taxonomy code"),
            "the emitted doc must not claim a dispatch that does not exist:\n{out}"
        );
    }

    #[test]
    fn error_with_message_returns_unknown_when_no_errors_declared() {
        let mut out = String::new();
        emit_helpers("crate", &[], &mut out);
        assert!(
            out.contains("inline fn _error_with_message(comptime E: type) E {"),
            "missing _error_with_message decl:\n{out}"
        );
        assert!(
            !out.contains("_from_ffi_msg_"),
            "no per-error matcher should be referenced when none are declared:\n{out}"
        );
        assert!(
            !out.contains("_first_error"),
            "the first-declared-variant fallback must not be emitted at all:\n{out}"
        );
        assert!(
            out.contains("    return error.UnknownFfiError;\n"),
            "unknown-error fallback required:\n{out}"
        );
        assert!(
            !out.contains("const code ="),
            "no taxonomy code is read when nothing dispatches on it (unused local):\n{out}"
        );
    }
}