use crate::codegen::c_consumer;
use crate::core::config::Language;
use crate::core::ir::ErrorDef;
use crate::docs::clean_doc;
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");
}
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}"
);
}
#[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}"
);
}
}