alef 0.115.0

Opinionated polyglot binding generator for Rust libraries
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
//! Helper functions for Zig code generation.
//!
//! Provides utilities for FFI introspection and documentation emission.

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;

/// 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`: returns the binding-owned copy of the last captured error
///   message, if any. `_capture_error_message` populates that copy from the
///   FFI layer's thread-local state *before* `_error_with_message` returns —
///   every generated wrapper function's deferred `_free` calls run once the
///   error value is produced but before the caller regains control, and each
///   `_free` re-enters the FFI layer through `catch_ffi_panic`, which clears
///   that thread-local state on entry. Reading it lazily from `_last_error`
///   after the wrapper returns would therefore observe an already-cleared
///   (and potentially freed) buffer; capturing eagerly, before any deferred
///   call can run, is mandatory.
///
/// `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",
        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;
                };
                // ~keep Must use the CASED helper, matching `errors.rs::zig_error_variant_component`
                // exactly: this dispatch emits `error.{variant_name}` against the error set that
                // file declares. `ErrorVariant.name` is the bare extracted ident, so it can be
                // snake_case; if the two disagree on casing the generated Zig names a member the
                // error set does not contain and fails to compile (alef issue #467).
                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
}

/// 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 {
            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
    }

    /// The FFI layer captures `status_code`, `retry_after` and the other introspection methods
    /// with the failing call; the deferred `_free` calls then clear that state. The helper must
    /// copy every getter into binding-owned storage before returning the error. ~keep
    #[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}");
        }
    }

    /// An error with no capturable method has nothing to copy, so no details type is emitted.
    #[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}"
        );
    }

    /// 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}"
        );
    }

    /// Regression: `_error_with_message` used to discard the FFI error message with
    /// `_ = _last_error();` when no declared variant carried a taxonomy code. Every generated
    /// wrapper function's deferred `_free` calls run before the caller regains control and
    /// re-enter the FFI layer, which clears the thread-local error state on entry -- so by the
    /// time a caller invoked `_last_error()` afterwards, the message was already gone. The fix
    /// captures the message into binding-owned storage inside `_error_with_message`, before any
    /// deferred call can run. ~keep
    #[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}"
        );
    }

    /// FFI code 5 (`Cancelled`) is infrastructure, not a declared variant, so it must map to a
    /// dedicated error whether or not any variant carries a taxonomy code -- previously both
    /// shapes reported it as `error.UnknownFfiError`. ~keep
    #[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}"
        );
    }
}