Skip to main content

alef_backend_zig/
trait_bridge.rs

1//! Zig trait-bridge code generation.
2//!
3//! Emits one Zig extern struct (vtable) and one registration wrapper function
4//! per configured `[[trait_bridges]]` entry.  The Zig consumer fills in the
5//! struct with `callconv(.C)` function pointers and calls `register_*`.
6//!
7//! # C symbol convention
8//!
9//! The generated `register_{trait_snake}` shim calls
10//! `c.{prefix}_register_{trait_snake}` — the symbol exposed by the
11//! `kreuzberg-ffi` C layer (pattern: `{crate_prefix}_register_{trait_snake}`).
12//! If the actual symbol differs, override the generated call site.
13//!
14//! # `TraitBridgeGenerator` implementation
15//!
16//! [`ZigTraitBridgeGenerator`] implements the shared [`TraitBridgeGenerator`]
17//! trait so that the shared codegen driver can invoke the Zig-specific
18//! `gen_unregistration_fn` and `gen_clear_fn` overrides.  The other required
19//! methods are stubs — Zig code is produced through the standalone
20//! [`emit_trait_bridge`] free function, not the shared driver.
21
22use alef_codegen::generators::trait_bridge::{TraitBridgeGenerator, TraitBridgeSpec};
23use alef_core::config::TraitBridgeConfig;
24use alef_core::ir::{MethodDef, TypeDef, TypeRef};
25use heck::ToSnakeCase;
26
27/// Zig type string to use for a vtable slot parameter or return type.
28///
29/// All string/complex types collapse to `[*c]const u8` (C string pointer) since
30/// the vtable slots use the raw C ABI — not the Zig-friendly wrapper layer.
31fn vtable_param_type(ty: &TypeRef) -> &'static str {
32    match ty {
33        TypeRef::Primitive(p) => {
34            use alef_core::ir::PrimitiveType::*;
35            match p {
36                Bool => "i32",
37                U8 => "u8",
38                U16 => "u16",
39                U32 => "u32",
40                U64 => "u64",
41                I8 => "i8",
42                I16 => "i16",
43                I32 => "i32",
44                I64 => "i64",
45                F32 => "f32",
46                F64 => "f64",
47                Usize => "usize",
48                Isize => "isize",
49            }
50        }
51        TypeRef::Unit => "void",
52        TypeRef::Duration => "i64",
53        // All string/path/complex types become C string pointers at the C ABI boundary.
54        _ => "[*c]const u8",
55    }
56}
57
58/// Zig return type for a vtable slot.
59///
60/// Fallible methods always return `i32` (0 = success, non-zero = error).
61/// Unit infallible methods return `void`.  Other infallible returns use the
62/// primitive mapping.
63fn vtable_return_type(method: &MethodDef) -> String {
64    if method.error_type.is_some() {
65        "i32".to_string()
66    } else {
67        vtable_param_type(&method.return_type).to_string()
68    }
69}
70
71/// Build a snake_case trait name from a PascalCase trait name.
72///
73/// Uses `heck::ToSnakeCase`, matching the pattern used by Go/C# backends.
74fn trait_snake(trait_name: &str) -> String {
75    trait_name.to_snake_case()
76}
77
78/// Emit a Zig param name for the C-ABI slot, expanding `Bytes` to ptr+len.
79///
80/// Returns a list of `(c_param_name, c_param_type)` pairs.
81fn vtable_c_params(method: &MethodDef) -> Vec<(String, String)> {
82    let mut params = vec![("ud".to_string(), "?*anyopaque".to_string())];
83    for p in &method.params {
84        if matches!(p.ty, TypeRef::Bytes) {
85            params.push((format!("{}_ptr", p.name), "[*c]const u8".to_string()));
86            params.push((format!("{}_len", p.name), "usize".to_string()));
87        } else {
88            params.push((p.name.clone(), vtable_param_type(&p.ty).to_string()));
89        }
90    }
91    if method.error_type.is_some() {
92        if !matches!(method.return_type, TypeRef::Unit) {
93            params.push(("out_result".to_string(), "?*?[*c]u8".to_string()));
94        }
95        params.push(("out_error".to_string(), "?*?[*c]u8".to_string()));
96    } else if !matches!(method.return_type, TypeRef::Unit) {
97        params.push(("out_result".to_string(), "?*?[*c]u8".to_string()));
98    }
99    params
100}
101
102/// Emit a `make_{trait_snake}_vtable(comptime T: type, instance: *T) I{Trait}` helper.
103///
104/// The helper builds `callconv(.C)` thunks for every vtable slot so the consumer
105/// only needs to write plain Zig methods on their type.
106///
107/// # Limitations
108///
109/// - Methods returning non-unit values through `out_result` use `unreachable` for
110///   the conversion path when the type cannot be expressed as a direct C primitive
111///   (complex types are documented as requiring manual implementation).
112/// - Lifecycle slots (`name_fn`, `version_fn`, `initialize_fn`, `shutdown_fn`) are
113///   emitted with `unreachable` bodies as stubs — the consumer overrides the
114///   relevant field in the returned vtable if needed.
115pub fn emit_make_vtable(trait_name: &str, has_super_trait: bool, trait_def: &TypeDef, out: &mut String) {
116    let snake = trait_snake(trait_name);
117
118    out.push_str(&crate::template_env::render(
119        "vtable_header_doc.jinja",
120        minijinja::context! {
121            trait_name => trait_name,
122            snake => &snake,
123        },
124    ));
125    out.push_str(&crate::template_env::render(
126        "vtable_impl_method.jinja",
127        minijinja::context! {
128            snake => &snake,
129            trait_name => trait_name,
130        },
131    ));
132    out.push_str(&crate::template_env::render(
133        "vtable_make_fn_header.jinja",
134        minijinja::context! {
135            trait_name => trait_name,
136        },
137    ));
138
139    // Lifecycle stubs when super_trait is present
140    if has_super_trait {
141        out.push_str(&crate::template_env::render(
142            "vtable_field_name_fn.jinja",
143            minijinja::context! {},
144        ));
145        out.push_str(&crate::template_env::render(
146            "vtable_field_version_fn.jinja",
147            minijinja::context! {},
148        ));
149        out.push_str(&crate::template_env::render(
150            "vtable_field_initialize_fn.jinja",
151            minijinja::context! {},
152        ));
153        out.push_str(&crate::template_env::render(
154            "vtable_field_shutdown_fn.jinja",
155            minijinja::context! {},
156        ));
157    }
158
159    // Per-method thunks
160    for method in &trait_def.methods {
161        let method_snake = method.name.to_snake_case();
162        let c_params = vtable_c_params(method);
163        let ret = vtable_return_type(method);
164
165        // Build the thunk parameter list string
166        let params_str = c_params
167            .iter()
168            .map(|(name, ty)| format!("{name}: {ty}"))
169            .collect::<Vec<_>>()
170            .join(", ");
171
172        out.push_str(&crate::template_env::render(
173            "vtable_instance_field.jinja",
174            minijinja::context! {
175                method_snake => &method_snake,
176                params_str => &params_str,
177                ret => &ret,
178            },
179        ));
180
181        // Cast user_data to *T
182        out.push_str("                const self: *T = @ptrCast(@alignCast(ud));\n");
183
184        // Reconstruct Bytes slices and build forwarding arg list
185        let mut call_args: Vec<String> = Vec::new();
186        for p in &method.params {
187            if matches!(p.ty, TypeRef::Bytes) {
188                out.push_str(&crate::template_env::render(
189                    "thunk_bytes_slice.jinja",
190                    minijinja::context! {
191                        slice_name => format!("{}_slice", p.name),
192                        ptr_name => format!("{}_ptr", p.name),
193                        len_name => format!("{}_len", p.name),
194                    },
195                ));
196                call_args.push(format!("{}_slice", p.name));
197            } else {
198                call_args.push(p.name.clone());
199            }
200        }
201
202        let args_str = call_args.join(", ");
203
204        // Pick a capture name for the success branch that won't collide with method
205        // params. Methods can have a param literally called `result`; using that as
206        // the unwrap binding shadows the outer scope (zig 0.16+ flags this).
207        let ok_binding = if method.params.iter().any(|p| p.name == "value") {
208            "ok_value"
209        } else {
210            "value"
211        };
212
213        if method.error_type.is_some() {
214            // Fallible method: call returns error union, write out_result/out_error
215            let has_result_out = !matches!(method.return_type, TypeRef::Unit);
216            out.push_str(&crate::template_env::render(
217                "thunk_fn_signature.jinja",
218                minijinja::context! {
219                    method_snake => &method_snake,
220                    args_str => &args_str,
221                    ok_binding => &ok_binding,
222                },
223            ));
224            // Write result via out_result pointer — for complex types this is unreachable.
225            // `unreachable` diverges, so any code after it (including `return 0;`) would
226            // be flagged "unreachable code" by zig 0.16+; only emit the trailing return
227            // when the success path actually flows through.
228            let mut success_path_diverges = false;
229            if has_result_out {
230                match &method.return_type {
231                    TypeRef::Primitive(_) | TypeRef::Unit => {
232                        out.push_str(&crate::template_env::render(
233                            "thunk_result_assign.jinja",
234                            minijinja::context! {
235                                ok_binding => &ok_binding,
236                            },
237                        ));
238                    }
239                    _ => {
240                        // String/Bytes/complex: cannot safely convert without allocator context
241                        out.push_str(&crate::template_env::render(
242                            "thunk_if_fallible.jinja",
243                            minijinja::context! {
244                                ok_binding => &ok_binding,
245                            },
246                        ));
247                        success_path_diverges = true;
248                    }
249                }
250            } else {
251                // Unit return on success — discard the captured Void to silence unused-variable.
252                out.push_str(&crate::template_env::render(
253                    "thunk_if_ok_result.jinja",
254                    minijinja::context! {
255                        ok_binding => &ok_binding,
256                    },
257                ));
258            }
259            if !success_path_diverges {
260                out.push_str("                    return 0;\n");
261            }
262            out.push_str("                } else |err| {\n");
263            out.push_str("                    _ = err;\n");
264            out.push_str("                    if (out_error) |ptr| ptr.* = null; // caller checks error code\n");
265            out.push_str("                    return 1;\n");
266            out.push_str("                }\n");
267        } else {
268            // Infallible non-Unit methods get an `out_result` param "for uniformity"
269            // (see vtable_c_params), but the body returns the value directly via the
270            // function return type — so the param is unused. Discard it so zig 0.16+
271            // doesn't flag "unused function parameter".
272            if !matches!(method.return_type, TypeRef::Unit) {
273                out.push_str("                _ = out_result;\n");
274            }
275            match &method.return_type {
276                TypeRef::Unit => {
277                    out.push_str(&crate::template_env::render(
278                        "thunk_if_error.jinja",
279                        minijinja::context! {
280                            method_snake => &method_snake,
281                            args_str => &args_str,
282                        },
283                    ));
284                }
285                TypeRef::Primitive(_) => {
286                    out.push_str(&crate::template_env::render(
287                        "thunk_infallible_return.jinja",
288                        minijinja::context! {
289                            method_snake => &method_snake,
290                            args_str => &args_str,
291                        },
292                    ));
293                }
294                _ => {
295                    // Non-unit infallible non-primitive: pass through (e.g., [*c]const u8)
296                    out.push_str(&crate::template_env::render(
297                        "thunk_infallible_return.jinja",
298                        minijinja::context! {
299                            method_snake => &method_snake,
300                            args_str => &args_str,
301                        },
302                    ));
303                }
304            }
305        }
306
307        out.push_str("            }\n");
308        out.push_str("        }.thunk,\n");
309        out.push('\n');
310    }
311
312    // free_user_data stub — does nothing by default; caller overrides if needed
313    out.push_str(&crate::template_env::render(
314        "vtable_free_user_data.jinja",
315        minijinja::context! {},
316    ));
317
318    out.push_str("    };\n");
319    out.push_str("}\n");
320}
321
322/// Emit the vtable extern struct and registration shim for a single trait bridge.
323///
324/// `prefix` is the C FFI prefix (e.g., `"kreuzberg"`).
325/// `bridge_cfg` is the trait bridge configuration entry.
326/// `trait_def` is the IR type definition for the trait (must have `is_trait = true`).
327/// `out` is the output buffer to append to.
328pub fn emit_trait_bridge(prefix: &str, bridge_cfg: &TraitBridgeConfig, trait_def: &TypeDef, out: &mut String) {
329    let trait_name = &trait_def.name;
330    let snake = trait_snake(trait_name);
331    let has_super_trait = bridge_cfg.super_trait.is_some();
332
333    // -------------------------------------------------------------------------
334    // Vtable struct: I{Trait}
335    // -------------------------------------------------------------------------
336    out.push_str(&crate::template_env::render(
337        "trait_vtable_header.jinja",
338        minijinja::context! {
339            trait_name => trait_name,
340            snake => &snake,
341        },
342    ));
343    out.push_str(&crate::template_env::render(
344        "trait_struct_header.jinja",
345        minijinja::context! {
346            trait_name => trait_name,
347        },
348    ));
349
350    // Plugin lifecycle slots — always present when a super_trait is configured.
351    if has_super_trait {
352        out.push_str("    /// Return the plugin name into `out_name` (heap-allocated, caller frees).\n");
353        out.push_str(
354            "    name_fn: ?*const fn (user_data: ?*anyopaque, out_name: ?*?[*c]u8) callconv(.C) void = null,\n",
355        );
356        out.push('\n');
357
358        out.push_str("    /// Return the plugin version into `out_version` (heap-allocated, caller frees).\n");
359        out.push_str(
360            "    version_fn: ?*const fn (user_data: ?*anyopaque, out_version: ?*?[*c]u8) callconv(.C) void = null,\n",
361        );
362        out.push('\n');
363
364        out.push_str("    /// Initialise the plugin; return 0 on success, non-zero on error.\n");
365        out.push_str(
366            "    initialize_fn: ?*const fn (user_data: ?*anyopaque, out_error: ?*?[*c]u8) callconv(.C) i32 = null,\n",
367        );
368        out.push('\n');
369
370        out.push_str("    /// Shut down the plugin; return 0 on success, non-zero on error.\n");
371        out.push_str(
372            "    shutdown_fn: ?*const fn (user_data: ?*anyopaque, out_error: ?*?[*c]u8) callconv(.C) i32 = null,\n",
373        );
374        out.push('\n');
375    }
376
377    // Trait method slots
378    for method in &trait_def.methods {
379        if !method.doc.is_empty() {
380            for line in method.doc.lines() {
381                out.push_str(&crate::template_env::render(
382                    "trait_method_doc.jinja",
383                    minijinja::context! {
384                        line => line,
385                    },
386                ));
387            }
388        }
389
390        let ret = vtable_return_type(method);
391        let method_snake = method.name.to_snake_case();
392
393        // Build the parameter list: user_data first, then method params.
394        let mut params = vec!["user_data: ?*anyopaque".to_string()];
395        for p in &method.params {
396            let ty = vtable_param_type(&p.ty);
397            // Bytes expand to two args (ptr + len)
398            if matches!(p.ty, TypeRef::Bytes) {
399                params.push(format!("{}_ptr: [*c]const u8", p.name));
400                params.push(format!("{}_len: usize", p.name));
401            } else {
402                params.push(format!("{}: {ty}", p.name));
403            }
404        }
405
406        // Fallible methods get out-result and out-error pointers.
407        if method.error_type.is_some() {
408            if !matches!(method.return_type, TypeRef::Unit) {
409                params.push("out_result: ?*?[*c]u8".to_string());
410            }
411            params.push("out_error: ?*?[*c]u8".to_string());
412        } else if !matches!(method.return_type, TypeRef::Unit) {
413            // Infallible non-void: return via out_result too for uniformity
414            params.push("out_result: ?*?[*c]u8".to_string());
415        }
416
417        let params_str = params.join(", ");
418        out.push_str(&crate::template_env::render(
419            "trait_method_signature.jinja",
420            minijinja::context! {
421                method_snake => &method_snake,
422                params_str => &params_str,
423                ret => &ret,
424            },
425        ));
426    }
427
428    // free_user_data — always last; called by Rust Drop to release the Zig-side handle.
429    out.push_str("    /// Called by the Rust runtime when the bridge is dropped.\n");
430    out.push_str("    /// Use this to release any Zig-side state held via `user_data`.\n");
431    out.push_str("    free_user_data: ?*const fn (user_data: ?*anyopaque) callconv(.C) void = null,\n");
432
433    out.push_str("};\n");
434    out.push('\n');
435
436    // -------------------------------------------------------------------------
437    // Registration shim: register_{trait_snake}
438    // -------------------------------------------------------------------------
439    let c_register = format!("c.{prefix}_register_{snake}");
440    let c_unregister = format!("c.{prefix}_unregister_{snake}");
441
442    out.push_str(&crate::template_env::render(
443        "register_fn_doc1.jinja",
444        minijinja::context! {
445            trait_name => trait_name,
446            snake => &snake,
447        },
448    ));
449    out.push_str(&crate::template_env::render(
450        "register_fn_signature.jinja",
451        minijinja::context! {
452            snake => &snake,
453            trait_name => trait_name,
454        },
455    ));
456    out.push_str(&crate::template_env::render(
457        "register_fn_body.jinja",
458        minijinja::context! {
459            c_register => &c_register,
460        },
461    ));
462    out.push_str("}\n");
463    out.push('\n');
464
465    // -------------------------------------------------------------------------
466    // Unregistration shim: unregister_{trait_snake}
467    // -------------------------------------------------------------------------
468    out.push_str(&crate::template_env::render(
469        "unregister_fn_doc.jinja",
470        minijinja::context! {
471            trait_name => trait_name,
472        },
473    ));
474    out.push_str(&crate::template_env::render(
475        "unregister_fn_signature.jinja",
476        minijinja::context! {
477            snake => &snake,
478        },
479    ));
480    out.push_str(&crate::template_env::render(
481        "unregister_fn_body.jinja",
482        minijinja::context! {
483            c_unregister => &c_unregister,
484        },
485    ));
486    out.push_str("}\n");
487    out.push('\n');
488
489    // -------------------------------------------------------------------------
490    // Comptime vtable builder: make_{trait_snake}_vtable
491    // -------------------------------------------------------------------------
492    emit_make_vtable(trait_name, has_super_trait, trait_def, out);
493}
494
495// ---------------------------------------------------------------------------
496// TraitBridgeGenerator implementation for the Zig backend
497// ---------------------------------------------------------------------------
498
499/// Zig-specific [`TraitBridgeGenerator`] implementation.
500///
501/// Carries the FFI symbol prefix (e.g., `"kreuzberg"`) used when deriving the
502/// C symbol for `unregister_*` and `clear_*` wrappers.
503///
504/// The required trait methods that produce *Rust* source (`gen_sync_method_body`,
505/// `gen_async_method_body`, `gen_constructor`, `gen_registration_fn`) return
506/// empty strings because Zig bridge code is produced by the standalone
507/// [`emit_trait_bridge`] free function, not the shared driver.
508pub struct ZigTraitBridgeGenerator {
509    /// FFI symbol prefix (e.g., `"kreuzberg"`).
510    pub prefix: String,
511}
512
513impl ZigTraitBridgeGenerator {
514    /// Construct a new generator for the given FFI symbol prefix.
515    pub fn new(prefix: impl Into<String>) -> Self {
516        Self { prefix: prefix.into() }
517    }
518}
519
520impl TraitBridgeGenerator for ZigTraitBridgeGenerator {
521    // ------------------------------------------------------------------
522    // Stub methods — Zig bridge code is emitted by `emit_trait_bridge`.
523    // ------------------------------------------------------------------
524
525    fn foreign_object_type(&self) -> &str {
526        ""
527    }
528
529    fn bridge_imports(&self) -> Vec<String> {
530        Vec::new()
531    }
532
533    fn gen_sync_method_body(&self, _method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
534        String::new()
535    }
536
537    fn gen_async_method_body(&self, _method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
538        String::new()
539    }
540
541    fn gen_constructor(&self, _spec: &TraitBridgeSpec) -> String {
542        String::new()
543    }
544
545    fn gen_registration_fn(&self, _spec: &TraitBridgeSpec) -> String {
546        String::new()
547    }
548
549    // ------------------------------------------------------------------
550    // Zig-specific overrides
551    // ------------------------------------------------------------------
552
553    /// Emit a Zig wrapper that calls `c.{prefix}_{unregister_fn}(name, out_error)`.
554    ///
555    /// Returns an empty string when `spec.bridge_config.unregister_fn` is `None`.
556    fn gen_unregistration_fn(&self, spec: &TraitBridgeSpec) -> String {
557        let Some(unregister_fn) = spec.bridge_config.unregister_fn.as_deref() else {
558            return String::new();
559        };
560        let c_unregister = format!("c.{}_{}", self.prefix, unregister_fn);
561
562        let mut out = String::new();
563        out.push_str(&crate::template_env::render(
564            "unregister_fn_doc.jinja",
565            minijinja::context! {
566                trait_name => spec.trait_def.name.as_str(),
567            },
568        ));
569        // Emit the signature directly: the configured `unregister_fn` is the
570        // complete Zig function name, not just the trait-snake suffix.
571        out.push_str(&format!(
572            "pub fn {unregister_fn}(name: [*c]const u8, out_error: ?*?[*c]u8) i32 {{\n"
573        ));
574        out.push_str(&crate::template_env::render(
575            "unregister_fn_body.jinja",
576            minijinja::context! {
577                c_unregister => &c_unregister,
578            },
579        ));
580        out.push_str("}\n");
581        out
582    }
583
584    /// Emit a Zig wrapper that calls `c.{prefix}_{clear_fn}(out_error)`.
585    ///
586    /// Returns an empty string when `spec.bridge_config.clear_fn` is `None`.
587    fn gen_clear_fn(&self, spec: &TraitBridgeSpec) -> String {
588        let Some(clear_fn) = spec.bridge_config.clear_fn.as_deref() else {
589            return String::new();
590        };
591        let c_clear = format!("c.{}_{}", self.prefix, clear_fn);
592
593        let mut out = String::new();
594        out.push_str(&crate::template_env::render(
595            "clear_fn_doc.jinja",
596            minijinja::context! {
597                trait_name => spec.trait_def.name.as_str(),
598            },
599        ));
600        out.push_str(&crate::template_env::render(
601            "clear_fn_signature.jinja",
602            minijinja::context! {
603                clear_fn => clear_fn,
604            },
605        ));
606        out.push_str(&crate::template_env::render(
607            "clear_fn_body.jinja",
608            minijinja::context! {
609                c_clear => &c_clear,
610            },
611        ));
612        out.push_str("}\n");
613        out
614    }
615}
616
617#[cfg(test)]
618mod tests {
619    use super::*;
620    use alef_core::ir::{FieldDef, MethodDef, ParamDef, PrimitiveType, ReceiverKind, TypeRef};
621
622    fn make_trait_def(name: &str, methods: Vec<MethodDef>) -> TypeDef {
623        TypeDef {
624            name: name.to_string(),
625            rust_path: format!("demo::{name}"),
626            original_rust_path: String::new(),
627            fields: Vec::<FieldDef>::new(),
628            methods,
629            is_opaque: true,
630            is_clone: false,
631            is_copy: false,
632            is_trait: true,
633            has_default: false,
634            has_stripped_cfg_fields: false,
635            is_return_type: false,
636            serde_rename_all: None,
637            has_serde: false,
638            super_traits: vec![],
639            doc: String::new(),
640            cfg: None,
641        }
642    }
643
644    fn make_method(name: &str, params: Vec<ParamDef>, return_type: TypeRef, error_type: Option<&str>) -> MethodDef {
645        MethodDef {
646            name: name.to_string(),
647            params,
648            return_type,
649            is_async: false,
650            is_static: false,
651            error_type: error_type.map(|s| s.to_string()),
652            doc: String::new(),
653            receiver: Some(ReceiverKind::Ref),
654            sanitized: false,
655            trait_source: None,
656            returns_ref: false,
657            returns_cow: false,
658            return_newtype_wrapper: None,
659            has_default_impl: false,
660        }
661    }
662
663    fn make_param(name: &str, ty: TypeRef) -> ParamDef {
664        ParamDef {
665            name: name.to_string(),
666            ty,
667            optional: false,
668            default: None,
669            sanitized: false,
670            typed_default: None,
671            is_ref: false,
672            is_mut: false,
673            newtype_wrapper: None,
674            original_type: None,
675        }
676    }
677
678    fn make_bridge_cfg(trait_name: &str, super_trait: Option<&str>) -> TraitBridgeConfig {
679        TraitBridgeConfig {
680            trait_name: trait_name.to_string(),
681            super_trait: super_trait.map(|s| s.to_string()),
682            registry_getter: None,
683            register_fn: None,
684
685            unregister_fn: None,
686
687            clear_fn: None,
688            type_alias: None,
689            param_name: None,
690            register_extra_args: None,
691            exclude_languages: vec![],
692            bind_via: alef_core::config::BridgeBinding::FunctionParam,
693            options_type: None,
694            options_field: None,
695        }
696    }
697
698    #[test]
699    fn single_method_trait_emits_vtable_and_register() {
700        let trait_def = make_trait_def(
701            "Validator",
702            vec![make_method(
703                "validate",
704                vec![make_param("input", TypeRef::String)],
705                TypeRef::Primitive(PrimitiveType::Bool),
706                None,
707            )],
708        );
709        let bridge_cfg = make_bridge_cfg("Validator", None);
710
711        let mut out = String::new();
712        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
713
714        // Vtable struct
715        assert!(
716            out.contains("pub const IValidator = extern struct {"),
717            "missing vtable struct: {out}"
718        );
719        // Method slot present
720        assert!(out.contains("validate:"), "missing validate slot: {out}");
721        // user_data first arg
722        assert!(out.contains("user_data: ?*anyopaque"), "missing user_data: {out}");
723        // callconv(.C) present
724        assert!(out.contains("callconv(.C)"), "missing callconv: {out}");
725        // free_user_data slot
726        assert!(out.contains("free_user_data:"), "missing free_user_data: {out}");
727        // Registration shim
728        assert!(out.contains("pub fn register_validator("), "missing register fn: {out}");
729        assert!(out.contains("c.demo_register_validator("), "wrong C symbol: {out}");
730        // Unregistration shim
731        assert!(
732            out.contains("pub fn unregister_validator("),
733            "missing unregister fn: {out}"
734        );
735        assert!(
736            out.contains("c.demo_unregister_validator("),
737            "wrong unregister C symbol: {out}"
738        );
739        // No plugin lifecycle when no super_trait
740        assert!(
741            !out.contains("name_fn:"),
742            "should not emit name_fn without super_trait: {out}"
743        );
744    }
745
746    #[test]
747    fn multi_method_trait_with_super_trait_emits_lifecycle_slots() {
748        let trait_def = make_trait_def(
749            "OcrBackend",
750            vec![
751                make_method(
752                    "process_image",
753                    vec![
754                        make_param("image_bytes", TypeRef::Bytes),
755                        make_param("config", TypeRef::String),
756                    ],
757                    TypeRef::String,
758                    Some("OcrError"),
759                ),
760                make_method(
761                    "supports_language",
762                    vec![make_param("lang", TypeRef::String)],
763                    TypeRef::Primitive(PrimitiveType::Bool),
764                    None,
765                ),
766            ],
767        );
768        let bridge_cfg = make_bridge_cfg("OcrBackend", Some("kreuzberg::plugins::Plugin"));
769
770        let mut out = String::new();
771        emit_trait_bridge("kreuzberg", &bridge_cfg, &trait_def, &mut out);
772
773        // Struct name
774        assert!(
775            out.contains("pub const IOcrBackend = extern struct {"),
776            "missing vtable: {out}"
777        );
778        // Plugin lifecycle slots emitted
779        assert!(out.contains("name_fn:"), "missing name_fn: {out}");
780        assert!(out.contains("version_fn:"), "missing version_fn: {out}");
781        assert!(out.contains("initialize_fn:"), "missing initialize_fn: {out}");
782        assert!(out.contains("shutdown_fn:"), "missing shutdown_fn: {out}");
783        // Trait method slots
784        assert!(out.contains("process_image:"), "missing process_image slot: {out}");
785        assert!(
786            out.contains("supports_language:"),
787            "missing supports_language slot: {out}"
788        );
789        // Bytes param expands to ptr + len
790        assert!(out.contains("image_bytes_ptr:"), "missing bytes ptr expansion: {out}");
791        assert!(out.contains("image_bytes_len:"), "missing bytes len expansion: {out}");
792        // Fallible method gets out_error
793        assert!(
794            out.contains("out_error:"),
795            "missing out_error for fallible method: {out}"
796        );
797        // C symbols use kreuzberg prefix
798        assert!(
799            out.contains("c.kreuzberg_register_ocr_backend("),
800            "wrong register symbol: {out}"
801        );
802        assert!(
803            out.contains("c.kreuzberg_unregister_ocr_backend("),
804            "wrong unregister symbol: {out}"
805        );
806        // Registration shim signature
807        assert!(
808            out.contains("pub fn register_ocr_backend("),
809            "missing register_ocr_backend fn: {out}"
810        );
811    }
812
813    // -----------------------------------------------------------------
814    // make_*_vtable tests
815    // -----------------------------------------------------------------
816
817    #[test]
818    fn make_vtable_emits_comptime_function_and_thunk() {
819        let trait_def = make_trait_def(
820            "Validator",
821            vec![make_method(
822                "validate",
823                vec![make_param("input", TypeRef::String)],
824                TypeRef::Primitive(PrimitiveType::Bool),
825                None,
826            )],
827        );
828        let bridge_cfg = make_bridge_cfg("Validator", None);
829
830        let mut out = String::new();
831        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
832
833        // Helper function declaration
834        assert!(
835            out.contains("pub fn make_validator_vtable(comptime T: type, instance: *T)"),
836            "missing make_validator_vtable: {out}"
837        );
838        // Returns the vtable type
839        assert!(out.contains("IValidator{"), "missing vtable literal: {out}");
840        // Thunk casts user_data
841        assert!(out.contains("@ptrCast(@alignCast(ud))"), "missing @ptrCast cast: {out}");
842        // callconv(.C) in thunk
843        assert!(out.contains("callconv(.C)"), "missing callconv(.C) in thunk: {out}");
844        // validate thunk field
845        assert!(out.contains(".validate ="), "missing .validate thunk field: {out}");
846        // free_user_data thunk
847        assert!(
848            out.contains(".free_user_data ="),
849            "missing .free_user_data thunk: {out}"
850        );
851        // No lifecycle stubs without super_trait
852        assert!(
853            !out.contains(".name_fn ="),
854            "must not emit .name_fn without super_trait: {out}"
855        );
856    }
857
858    #[test]
859    fn make_vtable_with_super_trait_emits_lifecycle_stubs() {
860        let trait_def = make_trait_def("OcrBackend", vec![]);
861        let bridge_cfg = make_bridge_cfg("OcrBackend", Some("kreuzberg::Plugin"));
862
863        let mut out = String::new();
864        emit_trait_bridge("kreuzberg", &bridge_cfg, &trait_def, &mut out);
865
866        assert!(
867            out.contains("pub fn make_ocr_backend_vtable(comptime T: type, instance: *T)"),
868            "missing make_ocr_backend_vtable: {out}"
869        );
870        assert!(out.contains(".name_fn ="), "missing .name_fn stub: {out}");
871        assert!(out.contains(".version_fn ="), "missing .version_fn stub: {out}");
872        assert!(out.contains(".initialize_fn ="), "missing .initialize_fn stub: {out}");
873        assert!(out.contains(".shutdown_fn ="), "missing .shutdown_fn stub: {out}");
874    }
875
876    #[test]
877    fn make_vtable_bytes_param_reconstructs_slice_in_thunk() {
878        let trait_def = make_trait_def(
879            "Processor",
880            vec![make_method(
881                "process",
882                vec![make_param("data", TypeRef::Bytes)],
883                TypeRef::Unit,
884                None,
885            )],
886        );
887        let bridge_cfg = make_bridge_cfg("Processor", None);
888
889        let mut out = String::new();
890        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
891
892        // Thunk receives ptr+len params
893        assert!(out.contains("data_ptr: [*c]const u8"), "missing data_ptr param: {out}");
894        assert!(out.contains("data_len: usize"), "missing data_len param: {out}");
895        // Thunk reconstructs slice
896        assert!(
897            out.contains("data_ptr[0..data_len]"),
898            "thunk must reconstruct slice from ptr+len: {out}"
899        );
900        // Thunk calls self.process with the slice
901        assert!(
902            out.contains("self.process(data_slice)"),
903            "thunk must call self.process: {out}"
904        );
905    }
906
907    #[test]
908    fn make_vtable_fallible_method_returns_i32_error_code() {
909        let trait_def = make_trait_def(
910            "Parser",
911            vec![make_method("parse", vec![], TypeRef::Unit, Some("ParseError"))],
912        );
913        let bridge_cfg = make_bridge_cfg("Parser", None);
914
915        let mut out = String::new();
916        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
917
918        // Thunk returns i32 (fallible → i32 return)
919        assert!(
920            out.contains("callconv(.C) i32"),
921            "fallible thunk must return i32: {out}"
922        );
923        // Returns 0 on success
924        assert!(out.contains("return 0;"), "must return 0 on success: {out}");
925        // Returns 1 on error
926        assert!(out.contains("return 1;"), "must return 1 on error: {out}");
927        // Error branch writes to out_error
928        assert!(out.contains("out_error"), "must write to out_error: {out}");
929    }
930
931    #[test]
932    fn make_vtable_primitive_return_passes_through() {
933        let trait_def = make_trait_def(
934            "Counter",
935            vec![make_method(
936                "count",
937                vec![],
938                TypeRef::Primitive(PrimitiveType::I32),
939                None,
940            )],
941        );
942        let bridge_cfg = make_bridge_cfg("demo", None);
943
944        let mut out = String::new();
945        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
946
947        // Infallible primitive method: thunk returns the value directly
948        assert!(
949            out.contains("return self.count()"),
950            "primitive return must be forwarded directly: {out}"
951        );
952    }
953
954    // -----------------------------------------------------------------
955    // ZigTraitBridgeGenerator tests
956    // -----------------------------------------------------------------
957
958    fn make_spec<'a>(trait_def: &'a TypeDef, bridge_cfg: &'a TraitBridgeConfig) -> TraitBridgeSpec<'a> {
959        use alef_codegen::generators::trait_bridge::TraitBridgeSpec;
960        use std::collections::HashMap;
961        TraitBridgeSpec {
962            trait_def,
963            bridge_config: bridge_cfg,
964            core_import: "kreuzberg",
965            wrapper_prefix: "Zig",
966            type_paths: HashMap::new(),
967            error_type: "KreuzbergError".to_string(),
968            error_constructor: "KreuzbergError::msg({msg})".to_string(),
969        }
970    }
971
972    #[test]
973    fn gen_unregistration_fn_emits_wrapper_when_configured() {
974        let trait_def = make_trait_def("OcrBackend", vec![]);
975        let mut bridge_cfg = make_bridge_cfg("OcrBackend", None);
976        bridge_cfg.unregister_fn = Some("unregister_ocr_backend".to_string());
977
978        let generator = ZigTraitBridgeGenerator::new("kreuzberg");
979        let spec = make_spec(&trait_def, &bridge_cfg);
980        let out = generator.gen_unregistration_fn(&spec);
981
982        assert!(!out.is_empty(), "expected non-empty output when unregister_fn is set");
983        assert!(
984            out.contains("pub fn unregister_ocr_backend("),
985            "wrong function name: {out}"
986        );
987        assert!(
988            out.contains("c.kreuzberg_unregister_ocr_backend("),
989            "wrong C symbol: {out}"
990        );
991        assert!(
992            out.contains("out_error: ?*?[*c]u8") || out.contains("out_error"),
993            "missing out_error param: {out}"
994        );
995        assert!(out.contains("return "), "missing return statement: {out}");
996        assert!(out.ends_with("}\n"), "missing closing brace: {out}");
997    }
998
999    #[test]
1000    fn gen_unregistration_fn_returns_empty_when_not_configured() {
1001        let trait_def = make_trait_def("OcrBackend", vec![]);
1002        let bridge_cfg = make_bridge_cfg("OcrBackend", None); // unregister_fn is None
1003
1004        let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1005        let spec = make_spec(&trait_def, &bridge_cfg);
1006        let out = generator.gen_unregistration_fn(&spec);
1007
1008        assert!(
1009            out.is_empty(),
1010            "expected empty output when unregister_fn is None, got: {out}"
1011        );
1012    }
1013
1014    #[test]
1015    fn gen_clear_fn_emits_wrapper_when_configured() {
1016        let trait_def = make_trait_def("OcrBackend", vec![]);
1017        let mut bridge_cfg = make_bridge_cfg("OcrBackend", None);
1018        bridge_cfg.clear_fn = Some("clear_ocr_backends".to_string());
1019
1020        let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1021        let spec = make_spec(&trait_def, &bridge_cfg);
1022        let out = generator.gen_clear_fn(&spec);
1023
1024        assert!(!out.is_empty(), "expected non-empty output when clear_fn is set");
1025        assert!(out.contains("pub fn clear_ocr_backends("), "wrong function name: {out}");
1026        assert!(out.contains("c.kreuzberg_clear_ocr_backends("), "wrong C symbol: {out}");
1027        assert!(
1028            out.contains("out_error: ?*?[*c]u8") || out.contains("out_error"),
1029            "missing out_error param: {out}"
1030        );
1031        assert!(out.contains("return "), "missing return statement: {out}");
1032        assert!(out.ends_with("}\n"), "missing closing brace: {out}");
1033    }
1034
1035    #[test]
1036    fn gen_clear_fn_returns_empty_when_not_configured() {
1037        let trait_def = make_trait_def("OcrBackend", vec![]);
1038        let bridge_cfg = make_bridge_cfg("OcrBackend", None); // clear_fn is None
1039
1040        let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1041        let spec = make_spec(&trait_def, &bridge_cfg);
1042        let out = generator.gen_clear_fn(&spec);
1043
1044        assert!(
1045            out.is_empty(),
1046            "expected empty output when clear_fn is None, got: {out}"
1047        );
1048    }
1049
1050    #[test]
1051    fn gen_unregistration_fn_uses_snake_case_function_name_verbatim() {
1052        // The configured `unregister_fn` name is used as-is (not re-derived from the trait).
1053        let trait_def = make_trait_def("DocumentExtractor", vec![]);
1054        let mut bridge_cfg = make_bridge_cfg("DocumentExtractor", None);
1055        bridge_cfg.unregister_fn = Some("unregister_extractor".to_string());
1056
1057        let generator = ZigTraitBridgeGenerator::new("demo");
1058        let spec = make_spec(&trait_def, &bridge_cfg);
1059        let out = generator.gen_unregistration_fn(&spec);
1060
1061        assert!(
1062            out.contains("pub fn unregister_extractor("),
1063            "must use configured fn name verbatim: {out}"
1064        );
1065        assert!(
1066            out.contains("c.demo_unregister_extractor("),
1067            "must use configured fn name in C symbol: {out}"
1068        );
1069    }
1070
1071    #[test]
1072    fn gen_clear_fn_uses_configured_fn_name_verbatim() {
1073        let trait_def = make_trait_def("DocumentExtractor", vec![]);
1074        let mut bridge_cfg = make_bridge_cfg("DocumentExtractor", None);
1075        bridge_cfg.clear_fn = Some("clear_all_extractors".to_string());
1076
1077        let generator = ZigTraitBridgeGenerator::new("demo");
1078        let spec = make_spec(&trait_def, &bridge_cfg);
1079        let out = generator.gen_clear_fn(&spec);
1080
1081        assert!(
1082            out.contains("pub fn clear_all_extractors("),
1083            "must use configured fn name verbatim: {out}"
1084        );
1085        assert!(
1086            out.contains("c.demo_clear_all_extractors("),
1087            "must use configured fn name in C symbol: {out}"
1088        );
1089    }
1090}