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            out.push_str(&crate::template_env::render(
381                "trait_method_doc_lines.jinja",
382                minijinja::context! {
383                    method_doc_lines => method.doc.lines().collect::<Vec<_>>(),
384                },
385            ));
386        }
387
388        let ret = vtable_return_type(method);
389        let method_snake = method.name.to_snake_case();
390
391        // Build the parameter list: user_data first, then method params.
392        let mut params = vec!["user_data: ?*anyopaque".to_string()];
393        for p in &method.params {
394            let ty = vtable_param_type(&p.ty);
395            // Bytes expand to two args (ptr + len)
396            if matches!(p.ty, TypeRef::Bytes) {
397                params.push(format!("{}_ptr: [*c]const u8", p.name));
398                params.push(format!("{}_len: usize", p.name));
399            } else {
400                params.push(format!("{}: {ty}", p.name));
401            }
402        }
403
404        // Fallible methods get out-result and out-error pointers.
405        if method.error_type.is_some() {
406            if !matches!(method.return_type, TypeRef::Unit) {
407                params.push("out_result: ?*?[*c]u8".to_string());
408            }
409            params.push("out_error: ?*?[*c]u8".to_string());
410        } else if !matches!(method.return_type, TypeRef::Unit) {
411            // Infallible non-void: return via out_result too for uniformity
412            params.push("out_result: ?*?[*c]u8".to_string());
413        }
414
415        let params_str = params.join(", ");
416        out.push_str(&crate::template_env::render(
417            "trait_method_signature.jinja",
418            minijinja::context! {
419                method_snake => &method_snake,
420                params_str => &params_str,
421                ret => &ret,
422            },
423        ));
424    }
425
426    // free_user_data — always last; called by Rust Drop to release the Zig-side handle.
427    out.push_str("    /// Called by the Rust runtime when the bridge is dropped.\n");
428    out.push_str("    /// Use this to release any Zig-side state held via `user_data`.\n");
429    out.push_str("    free_user_data: ?*const fn (user_data: ?*anyopaque) callconv(.C) void = null,\n");
430
431    out.push_str("};\n");
432    out.push('\n');
433
434    // -------------------------------------------------------------------------
435    // Registration shim: register_{trait_snake}
436    // -------------------------------------------------------------------------
437    let c_register = format!("c.{prefix}_register_{snake}");
438    let c_unregister = format!("c.{prefix}_unregister_{snake}");
439
440    out.push_str(&crate::template_env::render(
441        "register_fn_doc1.jinja",
442        minijinja::context! {
443            trait_name => trait_name,
444            snake => &snake,
445        },
446    ));
447    out.push_str(&crate::template_env::render(
448        "register_fn_signature.jinja",
449        minijinja::context! {
450            snake => &snake,
451            trait_name => trait_name,
452        },
453    ));
454    out.push_str(&crate::template_env::render(
455        "register_fn_body.jinja",
456        minijinja::context! {
457            c_register => &c_register,
458        },
459    ));
460    out.push_str("}\n");
461    out.push('\n');
462
463    // -------------------------------------------------------------------------
464    // Unregistration shim: unregister_{trait_snake}
465    // -------------------------------------------------------------------------
466    out.push_str(&crate::template_env::render(
467        "unregister_fn_doc.jinja",
468        minijinja::context! {
469            trait_name => trait_name,
470        },
471    ));
472    out.push_str(&crate::template_env::render(
473        "unregister_fn_signature.jinja",
474        minijinja::context! {
475            snake => &snake,
476        },
477    ));
478    out.push_str(&crate::template_env::render(
479        "unregister_fn_body.jinja",
480        minijinja::context! {
481            c_unregister => &c_unregister,
482        },
483    ));
484    out.push_str("}\n");
485    out.push('\n');
486
487    // -------------------------------------------------------------------------
488    // Comptime vtable builder: make_{trait_snake}_vtable
489    // -------------------------------------------------------------------------
490    emit_make_vtable(trait_name, has_super_trait, trait_def, out);
491}
492
493// ---------------------------------------------------------------------------
494// TraitBridgeGenerator implementation for the Zig backend
495// ---------------------------------------------------------------------------
496
497/// Zig-specific [`TraitBridgeGenerator`] implementation.
498///
499/// Carries the FFI symbol prefix (e.g., `"kreuzberg"`) used when deriving the
500/// C symbol for `unregister_*` and `clear_*` wrappers.
501///
502/// The required trait methods that produce *Rust* source (`gen_sync_method_body`,
503/// `gen_async_method_body`, `gen_constructor`, `gen_registration_fn`) return
504/// empty strings because Zig bridge code is produced by the standalone
505/// [`emit_trait_bridge`] free function, not the shared driver.
506pub struct ZigTraitBridgeGenerator {
507    /// FFI symbol prefix (e.g., `"kreuzberg"`).
508    pub prefix: String,
509}
510
511impl ZigTraitBridgeGenerator {
512    /// Construct a new generator for the given FFI symbol prefix.
513    pub fn new(prefix: impl Into<String>) -> Self {
514        Self { prefix: prefix.into() }
515    }
516}
517
518impl TraitBridgeGenerator for ZigTraitBridgeGenerator {
519    // ------------------------------------------------------------------
520    // Stub methods — Zig bridge code is emitted by `emit_trait_bridge`.
521    // ------------------------------------------------------------------
522
523    fn foreign_object_type(&self) -> &str {
524        ""
525    }
526
527    fn bridge_imports(&self) -> Vec<String> {
528        Vec::new()
529    }
530
531    fn gen_sync_method_body(&self, _method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
532        String::new()
533    }
534
535    fn gen_async_method_body(&self, _method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
536        String::new()
537    }
538
539    fn gen_constructor(&self, _spec: &TraitBridgeSpec) -> String {
540        String::new()
541    }
542
543    fn gen_registration_fn(&self, _spec: &TraitBridgeSpec) -> String {
544        String::new()
545    }
546
547    // ------------------------------------------------------------------
548    // Zig-specific overrides
549    // ------------------------------------------------------------------
550
551    /// Emit a Zig wrapper that calls `c.{prefix}_{unregister_fn}(name, out_error)`.
552    ///
553    /// Returns an empty string when `spec.bridge_config.unregister_fn` is `None`.
554    fn gen_unregistration_fn(&self, spec: &TraitBridgeSpec) -> String {
555        let Some(unregister_fn) = spec.bridge_config.unregister_fn.as_deref() else {
556            return String::new();
557        };
558        let c_unregister = format!("c.{}_{}", self.prefix, unregister_fn);
559
560        let mut out = String::new();
561        out.push_str(&crate::template_env::render(
562            "unregister_fn_doc.jinja",
563            minijinja::context! {
564                trait_name => spec.trait_def.name.as_str(),
565            },
566        ));
567        // Emit the signature directly: the configured `unregister_fn` is the
568        // complete Zig function name, not just the trait-snake suffix.
569        out.push_str(&crate::template_env::render(
570            "unregister_fn_configured_signature.jinja",
571            minijinja::context! {
572                unregister_fn => unregister_fn,
573            },
574        ));
575        out.push_str(&crate::template_env::render(
576            "unregister_fn_body.jinja",
577            minijinja::context! {
578                c_unregister => &c_unregister,
579            },
580        ));
581        out.push_str("}\n");
582        out
583    }
584
585    /// Emit a Zig wrapper that calls `c.{prefix}_{clear_fn}(out_error)`.
586    ///
587    /// Returns an empty string when `spec.bridge_config.clear_fn` is `None`.
588    fn gen_clear_fn(&self, spec: &TraitBridgeSpec) -> String {
589        let Some(clear_fn) = spec.bridge_config.clear_fn.as_deref() else {
590            return String::new();
591        };
592        let c_clear = format!("c.{}_{}", self.prefix, clear_fn);
593
594        let mut out = String::new();
595        out.push_str(&crate::template_env::render(
596            "clear_fn_doc.jinja",
597            minijinja::context! {
598                trait_name => spec.trait_def.name.as_str(),
599            },
600        ));
601        out.push_str(&crate::template_env::render(
602            "clear_fn_signature.jinja",
603            minijinja::context! {
604                clear_fn => clear_fn,
605            },
606        ));
607        out.push_str(&crate::template_env::render(
608            "clear_fn_body.jinja",
609            minijinja::context! {
610                c_clear => &c_clear,
611            },
612        ));
613        out.push_str("}\n");
614        out
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    use super::*;
621    use alef_core::ir::{FieldDef, MethodDef, ParamDef, PrimitiveType, ReceiverKind, TypeRef};
622
623    fn make_trait_def(name: &str, methods: Vec<MethodDef>) -> TypeDef {
624        TypeDef {
625            name: name.to_string(),
626            rust_path: format!("demo::{name}"),
627            original_rust_path: String::new(),
628            fields: Vec::<FieldDef>::new(),
629            methods,
630            is_opaque: true,
631            is_clone: false,
632            is_copy: false,
633            is_trait: true,
634            has_default: false,
635            has_stripped_cfg_fields: false,
636            is_return_type: false,
637            serde_rename_all: None,
638            has_serde: false,
639            super_traits: vec![],
640            doc: String::new(),
641            cfg: None,
642        }
643    }
644
645    fn make_method(name: &str, params: Vec<ParamDef>, return_type: TypeRef, error_type: Option<&str>) -> MethodDef {
646        MethodDef {
647            name: name.to_string(),
648            params,
649            return_type,
650            is_async: false,
651            is_static: false,
652            error_type: error_type.map(|s| s.to_string()),
653            doc: String::new(),
654            receiver: Some(ReceiverKind::Ref),
655            sanitized: false,
656            trait_source: None,
657            returns_ref: false,
658            returns_cow: false,
659            return_newtype_wrapper: None,
660            has_default_impl: false,
661        }
662    }
663
664    fn make_param(name: &str, ty: TypeRef) -> ParamDef {
665        ParamDef {
666            name: name.to_string(),
667            ty,
668            optional: false,
669            default: None,
670            sanitized: false,
671            typed_default: None,
672            is_ref: false,
673            is_mut: false,
674            newtype_wrapper: None,
675            original_type: None,
676        }
677    }
678
679    fn make_bridge_cfg(trait_name: &str, super_trait: Option<&str>) -> TraitBridgeConfig {
680        TraitBridgeConfig {
681            trait_name: trait_name.to_string(),
682            super_trait: super_trait.map(|s| s.to_string()),
683            registry_getter: None,
684            register_fn: None,
685
686            unregister_fn: None,
687
688            clear_fn: None,
689            type_alias: None,
690            param_name: None,
691            register_extra_args: None,
692            exclude_languages: vec![],
693            bind_via: alef_core::config::BridgeBinding::FunctionParam,
694            options_type: None,
695            options_field: None,
696            context_type: None,
697            result_type: None,
698            ffi_skip_methods: Vec::new(),
699        }
700    }
701
702    #[test]
703    fn single_method_trait_emits_vtable_and_register() {
704        let trait_def = make_trait_def(
705            "Validator",
706            vec![make_method(
707                "validate",
708                vec![make_param("input", TypeRef::String)],
709                TypeRef::Primitive(PrimitiveType::Bool),
710                None,
711            )],
712        );
713        let bridge_cfg = make_bridge_cfg("Validator", None);
714
715        let mut out = String::new();
716        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
717
718        // Vtable struct
719        assert!(
720            out.contains("pub const IValidator = extern struct {"),
721            "missing vtable struct: {out}"
722        );
723        // Method slot present
724        assert!(out.contains("validate:"), "missing validate slot: {out}");
725        // user_data first arg
726        assert!(out.contains("user_data: ?*anyopaque"), "missing user_data: {out}");
727        // callconv(.C) present
728        assert!(out.contains("callconv(.C)"), "missing callconv: {out}");
729        // free_user_data slot
730        assert!(out.contains("free_user_data:"), "missing free_user_data: {out}");
731        // Registration shim
732        assert!(out.contains("pub fn register_validator("), "missing register fn: {out}");
733        assert!(out.contains("c.demo_register_validator("), "wrong C symbol: {out}");
734        // Unregistration shim
735        assert!(
736            out.contains("pub fn unregister_validator("),
737            "missing unregister fn: {out}"
738        );
739        assert!(
740            out.contains("c.demo_unregister_validator("),
741            "wrong unregister C symbol: {out}"
742        );
743        // No plugin lifecycle when no super_trait
744        assert!(
745            !out.contains("name_fn:"),
746            "should not emit name_fn without super_trait: {out}"
747        );
748    }
749
750    #[test]
751    fn multi_method_trait_with_super_trait_emits_lifecycle_slots() {
752        let trait_def = make_trait_def(
753            "OcrBackend",
754            vec![
755                make_method(
756                    "process_image",
757                    vec![
758                        make_param("image_bytes", TypeRef::Bytes),
759                        make_param("config", TypeRef::String),
760                    ],
761                    TypeRef::String,
762                    Some("OcrError"),
763                ),
764                make_method(
765                    "supports_language",
766                    vec![make_param("lang", TypeRef::String)],
767                    TypeRef::Primitive(PrimitiveType::Bool),
768                    None,
769                ),
770            ],
771        );
772        let bridge_cfg = make_bridge_cfg("OcrBackend", Some("kreuzberg::plugins::Plugin"));
773
774        let mut out = String::new();
775        emit_trait_bridge("kreuzberg", &bridge_cfg, &trait_def, &mut out);
776
777        // Struct name
778        assert!(
779            out.contains("pub const IOcrBackend = extern struct {"),
780            "missing vtable: {out}"
781        );
782        // Plugin lifecycle slots emitted
783        assert!(out.contains("name_fn:"), "missing name_fn: {out}");
784        assert!(out.contains("version_fn:"), "missing version_fn: {out}");
785        assert!(out.contains("initialize_fn:"), "missing initialize_fn: {out}");
786        assert!(out.contains("shutdown_fn:"), "missing shutdown_fn: {out}");
787        // Trait method slots
788        assert!(out.contains("process_image:"), "missing process_image slot: {out}");
789        assert!(
790            out.contains("supports_language:"),
791            "missing supports_language slot: {out}"
792        );
793        // Bytes param expands to ptr + len
794        assert!(out.contains("image_bytes_ptr:"), "missing bytes ptr expansion: {out}");
795        assert!(out.contains("image_bytes_len:"), "missing bytes len expansion: {out}");
796        // Fallible method gets out_error
797        assert!(
798            out.contains("out_error:"),
799            "missing out_error for fallible method: {out}"
800        );
801        // C symbols use kreuzberg prefix
802        assert!(
803            out.contains("c.kreuzberg_register_ocr_backend("),
804            "wrong register symbol: {out}"
805        );
806        assert!(
807            out.contains("c.kreuzberg_unregister_ocr_backend("),
808            "wrong unregister symbol: {out}"
809        );
810        // Registration shim signature
811        assert!(
812            out.contains("pub fn register_ocr_backend("),
813            "missing register_ocr_backend fn: {out}"
814        );
815    }
816
817    // -----------------------------------------------------------------
818    // make_*_vtable tests
819    // -----------------------------------------------------------------
820
821    #[test]
822    fn make_vtable_emits_comptime_function_and_thunk() {
823        let trait_def = make_trait_def(
824            "Validator",
825            vec![make_method(
826                "validate",
827                vec![make_param("input", TypeRef::String)],
828                TypeRef::Primitive(PrimitiveType::Bool),
829                None,
830            )],
831        );
832        let bridge_cfg = make_bridge_cfg("Validator", None);
833
834        let mut out = String::new();
835        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
836
837        // Helper function declaration
838        assert!(
839            out.contains("pub fn make_validator_vtable(comptime T: type, instance: *T)"),
840            "missing make_validator_vtable: {out}"
841        );
842        // Returns the vtable type
843        assert!(out.contains("IValidator{"), "missing vtable literal: {out}");
844        // Thunk casts user_data
845        assert!(out.contains("@ptrCast(@alignCast(ud))"), "missing @ptrCast cast: {out}");
846        // callconv(.C) in thunk
847        assert!(out.contains("callconv(.C)"), "missing callconv(.C) in thunk: {out}");
848        // validate thunk field
849        assert!(out.contains(".validate ="), "missing .validate thunk field: {out}");
850        // free_user_data thunk
851        assert!(
852            out.contains(".free_user_data ="),
853            "missing .free_user_data thunk: {out}"
854        );
855        // No lifecycle stubs without super_trait
856        assert!(
857            !out.contains(".name_fn ="),
858            "must not emit .name_fn without super_trait: {out}"
859        );
860    }
861
862    #[test]
863    fn make_vtable_with_super_trait_emits_lifecycle_stubs() {
864        let trait_def = make_trait_def("OcrBackend", vec![]);
865        let bridge_cfg = make_bridge_cfg("OcrBackend", Some("kreuzberg::Plugin"));
866
867        let mut out = String::new();
868        emit_trait_bridge("kreuzberg", &bridge_cfg, &trait_def, &mut out);
869
870        assert!(
871            out.contains("pub fn make_ocr_backend_vtable(comptime T: type, instance: *T)"),
872            "missing make_ocr_backend_vtable: {out}"
873        );
874        assert!(out.contains(".name_fn ="), "missing .name_fn stub: {out}");
875        assert!(out.contains(".version_fn ="), "missing .version_fn stub: {out}");
876        assert!(out.contains(".initialize_fn ="), "missing .initialize_fn stub: {out}");
877        assert!(out.contains(".shutdown_fn ="), "missing .shutdown_fn stub: {out}");
878    }
879
880    #[test]
881    fn make_vtable_bytes_param_reconstructs_slice_in_thunk() {
882        let trait_def = make_trait_def(
883            "Processor",
884            vec![make_method(
885                "process",
886                vec![make_param("data", TypeRef::Bytes)],
887                TypeRef::Unit,
888                None,
889            )],
890        );
891        let bridge_cfg = make_bridge_cfg("Processor", None);
892
893        let mut out = String::new();
894        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
895
896        // Thunk receives ptr+len params
897        assert!(out.contains("data_ptr: [*c]const u8"), "missing data_ptr param: {out}");
898        assert!(out.contains("data_len: usize"), "missing data_len param: {out}");
899        // Thunk reconstructs slice
900        assert!(
901            out.contains("data_ptr[0..data_len]"),
902            "thunk must reconstruct slice from ptr+len: {out}"
903        );
904        // Thunk calls self.process with the slice
905        assert!(
906            out.contains("self.process(data_slice)"),
907            "thunk must call self.process: {out}"
908        );
909    }
910
911    #[test]
912    fn make_vtable_fallible_method_returns_i32_error_code() {
913        let trait_def = make_trait_def(
914            "Parser",
915            vec![make_method("parse", vec![], TypeRef::Unit, Some("ParseError"))],
916        );
917        let bridge_cfg = make_bridge_cfg("Parser", None);
918
919        let mut out = String::new();
920        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
921
922        // Thunk returns i32 (fallible → i32 return)
923        assert!(
924            out.contains("callconv(.C) i32"),
925            "fallible thunk must return i32: {out}"
926        );
927        // Returns 0 on success
928        assert!(out.contains("return 0;"), "must return 0 on success: {out}");
929        // Returns 1 on error
930        assert!(out.contains("return 1;"), "must return 1 on error: {out}");
931        // Error branch writes to out_error
932        assert!(out.contains("out_error"), "must write to out_error: {out}");
933    }
934
935    #[test]
936    fn make_vtable_primitive_return_passes_through() {
937        let trait_def = make_trait_def(
938            "Counter",
939            vec![make_method(
940                "count",
941                vec![],
942                TypeRef::Primitive(PrimitiveType::I32),
943                None,
944            )],
945        );
946        let bridge_cfg = make_bridge_cfg("demo", None);
947
948        let mut out = String::new();
949        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
950
951        // Infallible primitive method: thunk returns the value directly
952        assert!(
953            out.contains("return self.count()"),
954            "primitive return must be forwarded directly: {out}"
955        );
956    }
957
958    // -----------------------------------------------------------------
959    // ZigTraitBridgeGenerator tests
960    // -----------------------------------------------------------------
961
962    fn make_spec<'a>(trait_def: &'a TypeDef, bridge_cfg: &'a TraitBridgeConfig) -> TraitBridgeSpec<'a> {
963        use alef_codegen::generators::trait_bridge::TraitBridgeSpec;
964        use std::collections::HashMap;
965        TraitBridgeSpec {
966            trait_def,
967            bridge_config: bridge_cfg,
968            core_import: "kreuzberg",
969            wrapper_prefix: "Zig",
970            type_paths: HashMap::new(),
971            error_type: "KreuzbergError".to_string(),
972            error_constructor: "KreuzbergError::msg({msg})".to_string(),
973        }
974    }
975
976    #[test]
977    fn gen_unregistration_fn_emits_wrapper_when_configured() {
978        let trait_def = make_trait_def("OcrBackend", vec![]);
979        let mut bridge_cfg = make_bridge_cfg("OcrBackend", None);
980        bridge_cfg.unregister_fn = Some("unregister_ocr_backend".to_string());
981
982        let generator = ZigTraitBridgeGenerator::new("kreuzberg");
983        let spec = make_spec(&trait_def, &bridge_cfg);
984        let out = generator.gen_unregistration_fn(&spec);
985
986        assert!(!out.is_empty(), "expected non-empty output when unregister_fn is set");
987        assert!(
988            out.contains("pub fn unregister_ocr_backend("),
989            "wrong function name: {out}"
990        );
991        assert!(
992            out.contains("c.kreuzberg_unregister_ocr_backend("),
993            "wrong C symbol: {out}"
994        );
995        assert!(
996            out.contains("out_error: ?*?[*c]u8") || out.contains("out_error"),
997            "missing out_error param: {out}"
998        );
999        assert!(out.contains("return "), "missing return statement: {out}");
1000        assert!(out.ends_with("}\n"), "missing closing brace: {out}");
1001    }
1002
1003    #[test]
1004    fn gen_unregistration_fn_returns_empty_when_not_configured() {
1005        let trait_def = make_trait_def("OcrBackend", vec![]);
1006        let bridge_cfg = make_bridge_cfg("OcrBackend", None); // unregister_fn is None
1007
1008        let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1009        let spec = make_spec(&trait_def, &bridge_cfg);
1010        let out = generator.gen_unregistration_fn(&spec);
1011
1012        assert!(
1013            out.is_empty(),
1014            "expected empty output when unregister_fn is None, got: {out}"
1015        );
1016    }
1017
1018    #[test]
1019    fn gen_clear_fn_emits_wrapper_when_configured() {
1020        let trait_def = make_trait_def("OcrBackend", vec![]);
1021        let mut bridge_cfg = make_bridge_cfg("OcrBackend", None);
1022        bridge_cfg.clear_fn = Some("clear_ocr_backends".to_string());
1023
1024        let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1025        let spec = make_spec(&trait_def, &bridge_cfg);
1026        let out = generator.gen_clear_fn(&spec);
1027
1028        assert!(!out.is_empty(), "expected non-empty output when clear_fn is set");
1029        assert!(out.contains("pub fn clear_ocr_backends("), "wrong function name: {out}");
1030        assert!(out.contains("c.kreuzberg_clear_ocr_backends("), "wrong C symbol: {out}");
1031        assert!(
1032            out.contains("out_error: ?*?[*c]u8") || out.contains("out_error"),
1033            "missing out_error param: {out}"
1034        );
1035        assert!(out.contains("return "), "missing return statement: {out}");
1036        assert!(out.ends_with("}\n"), "missing closing brace: {out}");
1037    }
1038
1039    #[test]
1040    fn gen_clear_fn_returns_empty_when_not_configured() {
1041        let trait_def = make_trait_def("OcrBackend", vec![]);
1042        let bridge_cfg = make_bridge_cfg("OcrBackend", None); // clear_fn is None
1043
1044        let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1045        let spec = make_spec(&trait_def, &bridge_cfg);
1046        let out = generator.gen_clear_fn(&spec);
1047
1048        assert!(
1049            out.is_empty(),
1050            "expected empty output when clear_fn is None, got: {out}"
1051        );
1052    }
1053
1054    #[test]
1055    fn gen_unregistration_fn_uses_snake_case_function_name_verbatim() {
1056        // The configured `unregister_fn` name is used as-is (not re-derived from the trait).
1057        let trait_def = make_trait_def("DocumentExtractor", vec![]);
1058        let mut bridge_cfg = make_bridge_cfg("DocumentExtractor", None);
1059        bridge_cfg.unregister_fn = Some("unregister_extractor".to_string());
1060
1061        let generator = ZigTraitBridgeGenerator::new("demo");
1062        let spec = make_spec(&trait_def, &bridge_cfg);
1063        let out = generator.gen_unregistration_fn(&spec);
1064
1065        assert!(
1066            out.contains("pub fn unregister_extractor("),
1067            "must use configured fn name verbatim: {out}"
1068        );
1069        assert!(
1070            out.contains("c.demo_unregister_extractor("),
1071            "must use configured fn name in C symbol: {out}"
1072        );
1073    }
1074
1075    #[test]
1076    fn gen_clear_fn_uses_configured_fn_name_verbatim() {
1077        let trait_def = make_trait_def("DocumentExtractor", vec![]);
1078        let mut bridge_cfg = make_bridge_cfg("DocumentExtractor", None);
1079        bridge_cfg.clear_fn = Some("clear_all_extractors".to_string());
1080
1081        let generator = ZigTraitBridgeGenerator::new("demo");
1082        let spec = make_spec(&trait_def, &bridge_cfg);
1083        let out = generator.gen_clear_fn(&spec);
1084
1085        assert!(
1086            out.contains("pub fn clear_all_extractors("),
1087            "must use configured fn name verbatim: {out}"
1088        );
1089        assert!(
1090            out.contains("c.demo_clear_all_extractors("),
1091            "must use configured fn name in C symbol: {out}"
1092        );
1093    }
1094}