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        }
697    }
698
699    #[test]
700    fn single_method_trait_emits_vtable_and_register() {
701        let trait_def = make_trait_def(
702            "Validator",
703            vec![make_method(
704                "validate",
705                vec![make_param("input", TypeRef::String)],
706                TypeRef::Primitive(PrimitiveType::Bool),
707                None,
708            )],
709        );
710        let bridge_cfg = make_bridge_cfg("Validator", None);
711
712        let mut out = String::new();
713        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
714
715        // Vtable struct
716        assert!(
717            out.contains("pub const IValidator = extern struct {"),
718            "missing vtable struct: {out}"
719        );
720        // Method slot present
721        assert!(out.contains("validate:"), "missing validate slot: {out}");
722        // user_data first arg
723        assert!(out.contains("user_data: ?*anyopaque"), "missing user_data: {out}");
724        // callconv(.C) present
725        assert!(out.contains("callconv(.C)"), "missing callconv: {out}");
726        // free_user_data slot
727        assert!(out.contains("free_user_data:"), "missing free_user_data: {out}");
728        // Registration shim
729        assert!(out.contains("pub fn register_validator("), "missing register fn: {out}");
730        assert!(out.contains("c.demo_register_validator("), "wrong C symbol: {out}");
731        // Unregistration shim
732        assert!(
733            out.contains("pub fn unregister_validator("),
734            "missing unregister fn: {out}"
735        );
736        assert!(
737            out.contains("c.demo_unregister_validator("),
738            "wrong unregister C symbol: {out}"
739        );
740        // No plugin lifecycle when no super_trait
741        assert!(
742            !out.contains("name_fn:"),
743            "should not emit name_fn without super_trait: {out}"
744        );
745    }
746
747    #[test]
748    fn multi_method_trait_with_super_trait_emits_lifecycle_slots() {
749        let trait_def = make_trait_def(
750            "OcrBackend",
751            vec![
752                make_method(
753                    "process_image",
754                    vec![
755                        make_param("image_bytes", TypeRef::Bytes),
756                        make_param("config", TypeRef::String),
757                    ],
758                    TypeRef::String,
759                    Some("OcrError"),
760                ),
761                make_method(
762                    "supports_language",
763                    vec![make_param("lang", TypeRef::String)],
764                    TypeRef::Primitive(PrimitiveType::Bool),
765                    None,
766                ),
767            ],
768        );
769        let bridge_cfg = make_bridge_cfg("OcrBackend", Some("kreuzberg::plugins::Plugin"));
770
771        let mut out = String::new();
772        emit_trait_bridge("kreuzberg", &bridge_cfg, &trait_def, &mut out);
773
774        // Struct name
775        assert!(
776            out.contains("pub const IOcrBackend = extern struct {"),
777            "missing vtable: {out}"
778        );
779        // Plugin lifecycle slots emitted
780        assert!(out.contains("name_fn:"), "missing name_fn: {out}");
781        assert!(out.contains("version_fn:"), "missing version_fn: {out}");
782        assert!(out.contains("initialize_fn:"), "missing initialize_fn: {out}");
783        assert!(out.contains("shutdown_fn:"), "missing shutdown_fn: {out}");
784        // Trait method slots
785        assert!(out.contains("process_image:"), "missing process_image slot: {out}");
786        assert!(
787            out.contains("supports_language:"),
788            "missing supports_language slot: {out}"
789        );
790        // Bytes param expands to ptr + len
791        assert!(out.contains("image_bytes_ptr:"), "missing bytes ptr expansion: {out}");
792        assert!(out.contains("image_bytes_len:"), "missing bytes len expansion: {out}");
793        // Fallible method gets out_error
794        assert!(
795            out.contains("out_error:"),
796            "missing out_error for fallible method: {out}"
797        );
798        // C symbols use kreuzberg prefix
799        assert!(
800            out.contains("c.kreuzberg_register_ocr_backend("),
801            "wrong register symbol: {out}"
802        );
803        assert!(
804            out.contains("c.kreuzberg_unregister_ocr_backend("),
805            "wrong unregister symbol: {out}"
806        );
807        // Registration shim signature
808        assert!(
809            out.contains("pub fn register_ocr_backend("),
810            "missing register_ocr_backend fn: {out}"
811        );
812    }
813
814    // -----------------------------------------------------------------
815    // make_*_vtable tests
816    // -----------------------------------------------------------------
817
818    #[test]
819    fn make_vtable_emits_comptime_function_and_thunk() {
820        let trait_def = make_trait_def(
821            "Validator",
822            vec![make_method(
823                "validate",
824                vec![make_param("input", TypeRef::String)],
825                TypeRef::Primitive(PrimitiveType::Bool),
826                None,
827            )],
828        );
829        let bridge_cfg = make_bridge_cfg("Validator", None);
830
831        let mut out = String::new();
832        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
833
834        // Helper function declaration
835        assert!(
836            out.contains("pub fn make_validator_vtable(comptime T: type, instance: *T)"),
837            "missing make_validator_vtable: {out}"
838        );
839        // Returns the vtable type
840        assert!(out.contains("IValidator{"), "missing vtable literal: {out}");
841        // Thunk casts user_data
842        assert!(out.contains("@ptrCast(@alignCast(ud))"), "missing @ptrCast cast: {out}");
843        // callconv(.C) in thunk
844        assert!(out.contains("callconv(.C)"), "missing callconv(.C) in thunk: {out}");
845        // validate thunk field
846        assert!(out.contains(".validate ="), "missing .validate thunk field: {out}");
847        // free_user_data thunk
848        assert!(
849            out.contains(".free_user_data ="),
850            "missing .free_user_data thunk: {out}"
851        );
852        // No lifecycle stubs without super_trait
853        assert!(
854            !out.contains(".name_fn ="),
855            "must not emit .name_fn without super_trait: {out}"
856        );
857    }
858
859    #[test]
860    fn make_vtable_with_super_trait_emits_lifecycle_stubs() {
861        let trait_def = make_trait_def("OcrBackend", vec![]);
862        let bridge_cfg = make_bridge_cfg("OcrBackend", Some("kreuzberg::Plugin"));
863
864        let mut out = String::new();
865        emit_trait_bridge("kreuzberg", &bridge_cfg, &trait_def, &mut out);
866
867        assert!(
868            out.contains("pub fn make_ocr_backend_vtable(comptime T: type, instance: *T)"),
869            "missing make_ocr_backend_vtable: {out}"
870        );
871        assert!(out.contains(".name_fn ="), "missing .name_fn stub: {out}");
872        assert!(out.contains(".version_fn ="), "missing .version_fn stub: {out}");
873        assert!(out.contains(".initialize_fn ="), "missing .initialize_fn stub: {out}");
874        assert!(out.contains(".shutdown_fn ="), "missing .shutdown_fn stub: {out}");
875    }
876
877    #[test]
878    fn make_vtable_bytes_param_reconstructs_slice_in_thunk() {
879        let trait_def = make_trait_def(
880            "Processor",
881            vec![make_method(
882                "process",
883                vec![make_param("data", TypeRef::Bytes)],
884                TypeRef::Unit,
885                None,
886            )],
887        );
888        let bridge_cfg = make_bridge_cfg("Processor", None);
889
890        let mut out = String::new();
891        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
892
893        // Thunk receives ptr+len params
894        assert!(out.contains("data_ptr: [*c]const u8"), "missing data_ptr param: {out}");
895        assert!(out.contains("data_len: usize"), "missing data_len param: {out}");
896        // Thunk reconstructs slice
897        assert!(
898            out.contains("data_ptr[0..data_len]"),
899            "thunk must reconstruct slice from ptr+len: {out}"
900        );
901        // Thunk calls self.process with the slice
902        assert!(
903            out.contains("self.process(data_slice)"),
904            "thunk must call self.process: {out}"
905        );
906    }
907
908    #[test]
909    fn make_vtable_fallible_method_returns_i32_error_code() {
910        let trait_def = make_trait_def(
911            "Parser",
912            vec![make_method("parse", vec![], TypeRef::Unit, Some("ParseError"))],
913        );
914        let bridge_cfg = make_bridge_cfg("Parser", None);
915
916        let mut out = String::new();
917        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
918
919        // Thunk returns i32 (fallible → i32 return)
920        assert!(
921            out.contains("callconv(.C) i32"),
922            "fallible thunk must return i32: {out}"
923        );
924        // Returns 0 on success
925        assert!(out.contains("return 0;"), "must return 0 on success: {out}");
926        // Returns 1 on error
927        assert!(out.contains("return 1;"), "must return 1 on error: {out}");
928        // Error branch writes to out_error
929        assert!(out.contains("out_error"), "must write to out_error: {out}");
930    }
931
932    #[test]
933    fn make_vtable_primitive_return_passes_through() {
934        let trait_def = make_trait_def(
935            "Counter",
936            vec![make_method(
937                "count",
938                vec![],
939                TypeRef::Primitive(PrimitiveType::I32),
940                None,
941            )],
942        );
943        let bridge_cfg = make_bridge_cfg("demo", None);
944
945        let mut out = String::new();
946        emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
947
948        // Infallible primitive method: thunk returns the value directly
949        assert!(
950            out.contains("return self.count()"),
951            "primitive return must be forwarded directly: {out}"
952        );
953    }
954
955    // -----------------------------------------------------------------
956    // ZigTraitBridgeGenerator tests
957    // -----------------------------------------------------------------
958
959    fn make_spec<'a>(trait_def: &'a TypeDef, bridge_cfg: &'a TraitBridgeConfig) -> TraitBridgeSpec<'a> {
960        use alef_codegen::generators::trait_bridge::TraitBridgeSpec;
961        use std::collections::HashMap;
962        TraitBridgeSpec {
963            trait_def,
964            bridge_config: bridge_cfg,
965            core_import: "kreuzberg",
966            wrapper_prefix: "Zig",
967            type_paths: HashMap::new(),
968            error_type: "KreuzbergError".to_string(),
969            error_constructor: "KreuzbergError::msg({msg})".to_string(),
970        }
971    }
972
973    #[test]
974    fn gen_unregistration_fn_emits_wrapper_when_configured() {
975        let trait_def = make_trait_def("OcrBackend", vec![]);
976        let mut bridge_cfg = make_bridge_cfg("OcrBackend", None);
977        bridge_cfg.unregister_fn = Some("unregister_ocr_backend".to_string());
978
979        let generator = ZigTraitBridgeGenerator::new("kreuzberg");
980        let spec = make_spec(&trait_def, &bridge_cfg);
981        let out = generator.gen_unregistration_fn(&spec);
982
983        assert!(!out.is_empty(), "expected non-empty output when unregister_fn is set");
984        assert!(
985            out.contains("pub fn unregister_ocr_backend("),
986            "wrong function name: {out}"
987        );
988        assert!(
989            out.contains("c.kreuzberg_unregister_ocr_backend("),
990            "wrong C symbol: {out}"
991        );
992        assert!(
993            out.contains("out_error: ?*?[*c]u8") || out.contains("out_error"),
994            "missing out_error param: {out}"
995        );
996        assert!(out.contains("return "), "missing return statement: {out}");
997        assert!(out.ends_with("}\n"), "missing closing brace: {out}");
998    }
999
1000    #[test]
1001    fn gen_unregistration_fn_returns_empty_when_not_configured() {
1002        let trait_def = make_trait_def("OcrBackend", vec![]);
1003        let bridge_cfg = make_bridge_cfg("OcrBackend", None); // unregister_fn is None
1004
1005        let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1006        let spec = make_spec(&trait_def, &bridge_cfg);
1007        let out = generator.gen_unregistration_fn(&spec);
1008
1009        assert!(
1010            out.is_empty(),
1011            "expected empty output when unregister_fn is None, got: {out}"
1012        );
1013    }
1014
1015    #[test]
1016    fn gen_clear_fn_emits_wrapper_when_configured() {
1017        let trait_def = make_trait_def("OcrBackend", vec![]);
1018        let mut bridge_cfg = make_bridge_cfg("OcrBackend", None);
1019        bridge_cfg.clear_fn = Some("clear_ocr_backends".to_string());
1020
1021        let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1022        let spec = make_spec(&trait_def, &bridge_cfg);
1023        let out = generator.gen_clear_fn(&spec);
1024
1025        assert!(!out.is_empty(), "expected non-empty output when clear_fn is set");
1026        assert!(out.contains("pub fn clear_ocr_backends("), "wrong function name: {out}");
1027        assert!(out.contains("c.kreuzberg_clear_ocr_backends("), "wrong C symbol: {out}");
1028        assert!(
1029            out.contains("out_error: ?*?[*c]u8") || out.contains("out_error"),
1030            "missing out_error param: {out}"
1031        );
1032        assert!(out.contains("return "), "missing return statement: {out}");
1033        assert!(out.ends_with("}\n"), "missing closing brace: {out}");
1034    }
1035
1036    #[test]
1037    fn gen_clear_fn_returns_empty_when_not_configured() {
1038        let trait_def = make_trait_def("OcrBackend", vec![]);
1039        let bridge_cfg = make_bridge_cfg("OcrBackend", None); // clear_fn is None
1040
1041        let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1042        let spec = make_spec(&trait_def, &bridge_cfg);
1043        let out = generator.gen_clear_fn(&spec);
1044
1045        assert!(
1046            out.is_empty(),
1047            "expected empty output when clear_fn is None, got: {out}"
1048        );
1049    }
1050
1051    #[test]
1052    fn gen_unregistration_fn_uses_snake_case_function_name_verbatim() {
1053        // The configured `unregister_fn` name is used as-is (not re-derived from the trait).
1054        let trait_def = make_trait_def("DocumentExtractor", vec![]);
1055        let mut bridge_cfg = make_bridge_cfg("DocumentExtractor", None);
1056        bridge_cfg.unregister_fn = Some("unregister_extractor".to_string());
1057
1058        let generator = ZigTraitBridgeGenerator::new("demo");
1059        let spec = make_spec(&trait_def, &bridge_cfg);
1060        let out = generator.gen_unregistration_fn(&spec);
1061
1062        assert!(
1063            out.contains("pub fn unregister_extractor("),
1064            "must use configured fn name verbatim: {out}"
1065        );
1066        assert!(
1067            out.contains("c.demo_unregister_extractor("),
1068            "must use configured fn name in C symbol: {out}"
1069        );
1070    }
1071
1072    #[test]
1073    fn gen_clear_fn_uses_configured_fn_name_verbatim() {
1074        let trait_def = make_trait_def("DocumentExtractor", vec![]);
1075        let mut bridge_cfg = make_bridge_cfg("DocumentExtractor", None);
1076        bridge_cfg.clear_fn = Some("clear_all_extractors".to_string());
1077
1078        let generator = ZigTraitBridgeGenerator::new("demo");
1079        let spec = make_spec(&trait_def, &bridge_cfg);
1080        let out = generator.gen_clear_fn(&spec);
1081
1082        assert!(
1083            out.contains("pub fn clear_all_extractors("),
1084            "must use configured fn name verbatim: {out}"
1085        );
1086        assert!(
1087            out.contains("c.demo_clear_all_extractors("),
1088            "must use configured fn name in C symbol: {out}"
1089        );
1090    }
1091}