use crate::backends::zig::type_map::ZigMapper;
use crate::codegen::c_consumer;
use crate::codegen::error_gen::{LastErrorField, LastErrorFieldKind};
use crate::codegen::type_mapper::TypeMapper;
use crate::core::config::Language;
use crate::core::ir::ErrorDef;
use crate::core::ir::PrimitiveType;
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",
crate::alef_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",
crate::alef_context! {
free_symbol => free_symbol,
},
));
out.push_str("}\n\n");
out.push_str("/// Binding-owned copy of the last error message captured by\n");
out.push_str("/// `_capture_error_message`. Freed and replaced each time a new error is\n");
out.push_str("/// captured; outlives the deferred FFI calls that clear the underlying\n");
out.push_str("/// thread-local state in the FFI layer.\n");
out.push_str("threadlocal var _captured_error: ?[]u8 = null;\n\n");
out.push_str("/// Retrieve the last error message captured by `_error_with_message`, if any.\n");
out.push_str("/// Returns a slice into binding-owned storage, valid until the next captured\n");
out.push_str("/// error overwrites it.\n");
out.push_str("pub fn _last_error() ?[]const u8 {\n");
out.push_str(" return _captured_error;\n");
out.push_str("}\n\n");
let details = crate::codegen::error_gen::last_error_fields(declared_errors);
let mut capture_details = String::new();
if !details.is_empty() {
out.push_str(&details_type(&details));
out.push_str("/// Introspection values of the last captured error, copied from the FFI layer with the\n");
out.push_str("/// message. Durations are whole milliseconds. Text is binding-owned and valid until the\n");
out.push_str("/// next captured error replaces it.\n");
out.push_str("threadlocal var _captured_details: LastErrorDetails = .{};\n\n");
out.push_str("/// Retrieve the introspection values (HTTP status, retry delay, ...) of the last error\n");
out.push_str("/// captured by `_error_with_message`.\n");
out.push_str("pub fn _last_error_details() LastErrorDetails {\n");
out.push_str(" return _captured_details;\n");
out.push_str("}\n\n");
capture_details = capture_details_statements(prefix, &details);
}
out.push_str("/// Copy the FFI layer's current error message into binding-owned storage.\n");
out.push_str(&format!(
"/// Must run before any deferred `_free` call: every `{prefix}_*` FFI entry point\n"
));
out.push_str("/// clears the thread-local error state on entry (`catch_ffi_panic` opens with\n");
out.push_str("/// `clear_last_error()`), so a `_free` invoked between the failing call and this\n");
out.push_str("/// capture would wipe the message before it could be read.\n");
out.push_str("fn _capture_error_message() void {\n");
out.push_str(&crate::backends::zig::template_env::render(
"helper_last_error_code.jinja",
crate::alef_context! {
symbol => error_code_symbol,
},
));
out.push_str(" if (_code == 0) return;\n");
out.push_str(&capture_details);
out.push_str(&crate::backends::zig::template_env::render(
"helper_last_error_ctx.jinja",
crate::alef_context! {
symbol => error_context_symbol,
},
));
out.push_str(" if (_ctx == null) return;\n");
out.push_str(" const _msg = std.mem.sliceTo(_ctx, 0);\n");
out.push_str(" if (_captured_error) |_old| std.heap.c_allocator.free(_old);\n");
out.push_str(" _captured_error = std.heap.c_allocator.dupe(u8, _msg) catch null;\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("/// The FFI cancellation code maps to `error.Cancelled` ahead of any variant dispatch.\n");
out.push_str("inline fn _error_with_message(comptime E: type) E {\n");
out.push_str(" _capture_error_message();\n");
out.push_str(&format!(
" if (c.{error_code_symbol}() == {}) return error.Cancelled;\n",
crate::core::ir::ApiSurface::FFI_ERROR_CODE_CANCELLED
));
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::cased_public_type_name(Language::Zig, &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");
}
fn details_type(details: &[LastErrorField]) -> String {
let mut out =
String::from("/// Introspection values of an FFI error, one member for each capturable error method.\n");
out.push_str("pub const LastErrorDetails = struct {\n");
for field in details {
let (zig_type, default) = match &field.kind {
LastErrorFieldKind::Scalar(primitive) => {
let zig_type = ZigMapper.primitive(primitive).into_owned();
let default = if matches!(primitive, PrimitiveType::Bool) {
"false"
} else if matches!(primitive, PrimitiveType::F32 | PrimitiveType::F64) {
"0.0"
} else {
"0"
};
(zig_type, default)
}
LastErrorFieldKind::Text => ("?[]const u8".to_string(), "null"),
LastErrorFieldKind::DurationMillis => ("?u64".to_string(), "null"),
};
out.push_str(&format!(" {}: {zig_type} = {default},\n", field.name));
}
out.push_str("};\n\n");
out
}
fn capture_details_statements(prefix: &str, details: &[LastErrorField]) -> String {
let mut out = String::new();
for field in details {
let symbol = c_consumer::last_error_field_symbol(prefix, &field.name);
let name = &field.name;
match &field.kind {
LastErrorFieldKind::Scalar(_) => {
out.push_str(&format!(" _captured_details.{name} = c.{symbol}();\n"));
}
LastErrorFieldKind::Text => {
out.push_str(&format!(
" if (_captured_details.{name}) |_old| std.heap.c_allocator.free(_old);\n"
));
out.push_str(&format!(" const _{name}_ptr = c.{symbol}();\n"));
out.push_str(&format!(
" _captured_details.{name} = if (_{name}_ptr == null) null else std.heap.c_allocator.dupe(u8, std.mem.sliceTo(_{name}_ptr, 0)) catch null;\n"
));
}
LastErrorFieldKind::DurationMillis => {
out.push_str(&format!(" const _{name}_ms = c.{symbol}();\n"));
out.push_str(&format!(
" _captured_details.{name} = if (_{name}_ms >= 0) @as(u64, @intCast(_{name}_ms)) else null;\n"
));
}
}
}
out
}
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 {
exception_base: None,
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(),
}
}
fn introspective_error() -> ErrorDef {
use crate::core::ir::{MethodDef, PrimitiveType, TypeRef};
let method = |name: &str, return_type: TypeRef| MethodDef {
name: name.to_string(),
return_type,
..MethodDef::default()
};
let mut error = request_error(Some(100));
error.methods = vec![
method("status_code", TypeRef::Primitive(PrimitiveType::U16)),
method("is_transient", TypeRef::Primitive(PrimitiveType::Bool)),
method("error_type", TypeRef::String),
method("retry_after", TypeRef::Optional(Box::new(TypeRef::Duration))),
];
error
}
#[test]
fn capture_copies_every_error_introspection_getter_before_the_deferred_frees_run() {
let mut out = String::new();
emit_helpers("example_pack", &[introspective_error()], &mut out);
assert!(out.contains("pub const LastErrorDetails = struct {"), "{out}");
for declaration in [
" status_code: u16 = 0,\n",
" is_transient: bool = false,\n",
" error_type: ?[]const u8 = null,\n",
" retry_after: ?u64 = null,\n",
] {
assert!(out.contains(declaration), "missing {declaration:?}:\n{out}");
}
assert!(
out.contains("pub fn _last_error_details() LastErrorDetails {\n return _captured_details;\n}"),
"{out}"
);
let capture = out
.split("fn _capture_error_message() void {")
.nth(1)
.expect("capture fn");
let capture = capture.split("\n}\n").next().unwrap();
for read in [
"_captured_details.status_code = c.example_pack_last_error_status_code();",
"_captured_details.is_transient = c.example_pack_last_error_is_transient();",
"c.example_pack_last_error_error_type()",
"const _retry_after_ms = c.example_pack_last_error_retry_after();",
"_captured_details.retry_after = if (_retry_after_ms >= 0) @as(u64, @intCast(_retry_after_ms)) else null;",
] {
assert!(capture.contains(read), "capture must read {read:?}:\n{capture}");
}
}
#[test]
fn no_introspection_method_means_no_details_type() {
let mut out = String::new();
emit_helpers("example_pack", &[request_error(Some(100))], &mut out);
assert!(!out.contains("LastErrorDetails"), "{out}");
assert!(!out.contains("_captured_details"), "{out}");
}
#[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_captures_context_into_binding_owned_storage() {
let mut out = String::new();
emit_helpers("example_pack", &[], &mut out);
assert!(
!out.contains("_ = _last_error();"),
"the FFI error context must not be read and discarded:\n{out}"
);
assert!(
out.contains("threadlocal var _captured_error: ?[]u8 = null;"),
"missing binding-owned error message storage:\n{out}"
);
assert!(
out.contains("fn _capture_error_message() void {"),
"missing the capture routine:\n{out}"
);
assert!(
out.contains(" _capture_error_message();\n"),
"_error_with_message must capture before returning:\n{out}"
);
assert!(
out.contains("pub fn _last_error() ?[]const u8 {\n return _captured_error;\n}"),
"_last_error must read the binding-owned copy, not the FFI layer directly:\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}"
);
}
#[test]
fn error_with_message_maps_the_cancelled_code_with_and_without_dispatch() {
let mut bare = String::new();
emit_helpers("example_pack", &[], &mut bare);
let mut coded = String::new();
emit_helpers("example_pack", &[request_error(Some(100))], &mut coded);
for out in [&bare, &coded] {
assert!(
out.contains(" if (c.example_pack_last_error_code() == 5) return error.Cancelled;\n"),
"missing cancellation arm:\n{out}"
);
}
let cancelled = coded.find("error.Cancelled").expect("cancel arm");
let switch = coded.find("switch (code)").expect("dispatch switch");
assert!(
cancelled < switch,
"cancellation is checked before taxonomy dispatch:\n{coded}"
);
}
}