Skip to main content

alef/codegen/error_gen/
native.rs

1pub fn gen_magnus_error_methods_struct(error: &ErrorDef, core_import: &str) -> String {
2    if error.methods.is_empty() {
3        return String::new();
4    }
5
6    let rust_path = if error.rust_path.is_empty() {
7        format!("{core_import}::{}", error.name)
8    } else {
9        error.rust_path.replace('-', "_")
10    };
11
12    let struct_name = format!("{}Info", error.name);
13
14    let mut fields = Vec::new();
15    let mut methods = Vec::new();
16    let mut ctor_assignments = Vec::new();
17
18    for method in &error.methods {
19        match method.name.as_str() {
20            "status_code" => {
21                fields.push("    status_code: u16,".to_string());
22                methods.push(
23                    concat!(
24                        "    /// HTTP status code for this error (0 means no associated status).\n",
25                        "    pub fn status_code(&self) -> u16 {\n",
26                        "        self.status_code\n",
27                        "    }",
28                    )
29                    .to_string(),
30                );
31                ctor_assignments.push("        status_code: e.status_code(),".to_string());
32            }
33            "is_transient" => {
34                fields.push("    is_transient: bool,".to_string());
35                methods.push(
36                    concat!(
37                        "    /// Returns `true` if the error is transient and a retry may succeed.\n",
38                        "    pub fn transient(&self) -> bool {\n",
39                        "        self.is_transient\n",
40                        "    }",
41                    )
42                    .to_string(),
43                );
44                ctor_assignments.push("        is_transient: e.is_transient(),".to_string());
45            }
46            "error_type" => {
47                fields.push("    error_type: String,".to_string());
48                methods.push(
49                    concat!(
50                        "    /// Machine-readable error category string for matching and logging.\n",
51                        "    pub fn error_type(&self) -> String {\n",
52                        "        self.error_type.clone()\n",
53                        "    }",
54                    )
55                    .to_string(),
56                );
57                ctor_assignments.push("        error_type: e.error_type().to_string(),".to_string());
58            }
59            other => {
60                methods.push(format!("    // Not emitted: method `{other}` on `{struct_name}`"));
61            }
62        }
63    }
64
65    let struct_def = format!(
66        "#[magnus::wrap(class = \"{struct_name}\", free_immediately, size)]\npub struct {struct_name} {{\n{}\n}}",
67        fields.join("\n")
68    );
69
70    let from_fn = format!(
71        "#[allow(dead_code)]\nfn {snake_name}_info(e: &{rust_path}) -> {struct_name} {{\n    {struct_name} {{\n{}\n    }}\n}}",
72        ctor_assignments.join("\n"),
73        snake_name = to_snake_case(&error.name),
74    );
75
76    let impl_block = format!("impl {struct_name} {{\n{}\n}}", methods.join("\n\n"));
77
78    format!("{struct_def}\n\n{from_fn}\n\n{impl_block}")
79}
80
81/// Returns the `define_class` + `define_method` registration lines for the error info struct.
82pub fn magnus_error_methods_registrations(error: &ErrorDef) -> Vec<String> {
83    if error.methods.is_empty() {
84        return Vec::new();
85    }
86    let struct_name = format!("{}Info", error.name);
87    let snake = to_snake_case(&error.name);
88    let class_var = format!("{snake}_info_class");
89    let mut lines = Vec::new();
90    lines.push(format!(
91        "    let {class_var} = module.define_class(\"{struct_name}\", ruby.class_object())?;"
92    ));
93    for method in &error.methods {
94        let (ruby_name, rust_fn) = if method.name == "is_transient" {
95            ("transient?".to_string(), "transient".to_string())
96        } else {
97            (method.name.clone(), method.name.clone())
98        };
99        lines.push(format!(
100            "    {class_var}.define_method(\"{ruby_name}\", magnus::method!({struct_name}::{rust_fn}, 0))?;"
101        ));
102    }
103    lines
104}
105
106// ---------------------------------------------------------------------------
107// PHP (ext-php-rs) error generation
108// ---------------------------------------------------------------------------
109
110/// Generate a converter function that maps a core error to `PhpException`.
111pub fn gen_php_error_converter(error: &ErrorDef, core_import: &str) -> String {
112    let rust_path = if error.rust_path.is_empty() {
113        format!("{core_import}::{}", error.name)
114    } else {
115        error.rust_path.replace('-', "_")
116    };
117
118    let fn_name = format!("{}_to_php_err", to_snake_case(&error.name));
119
120    // Pre-compute (pattern, variant_name) pairs
121    let mut variants = Vec::new();
122    for variant in &error.variants {
123        let pattern = error_variant_wildcard_pattern(&rust_path, variant);
124        variants.push((pattern, variant.name.clone()));
125    }
126
127    crate::codegen::template_env::render(
128        "error_gen/php_error_converter.jinja",
129        minijinja::context! {
130            rust_path => rust_path.as_str(),
131            fn_name => fn_name.as_str(),
132            variants => variants,
133        },
134    )
135}
136
137/// Return the PHP converter function name for a given error type.
138pub fn php_converter_fn_name(error: &ErrorDef) -> String {
139    format!("{}_to_php_err", to_snake_case(&error.name))
140}
141
142/// Generate a `#[php_class]` + `#[php_impl]` block for the error type, storing
143/// the whitelisted introspection method return values as Rust fields exposed via
144/// `#[php_method]`.
145///
146/// Returns an empty string when `error.methods` is empty.
147pub fn gen_php_error_methods_impl(error: &ErrorDef, core_import: &str) -> String {
148    if error.methods.is_empty() {
149        return String::new();
150    }
151
152    let rust_path = if error.rust_path.is_empty() {
153        format!("{core_import}::{}", error.name)
154    } else {
155        error.rust_path.replace('-', "_")
156    };
157
158    let struct_name = format!("{}Info", error.name);
159
160    let mut fields = Vec::new();
161    let mut methods = Vec::new();
162    let mut ctor_assignments = Vec::new();
163
164    for method in &error.methods {
165        match method.name.as_str() {
166            "status_code" => {
167                fields.push("    pub status_code: u16,".to_string());
168                methods.push(
169                    concat!(
170                        "    /// HTTP status code for this error (0 means no associated status).\n",
171                        "    pub fn status_code(&self) -> u16 {\n",
172                        "        self.status_code\n",
173                        "    }",
174                    )
175                    .to_string(),
176                );
177                ctor_assignments.push("        status_code: e.status_code(),".to_string());
178            }
179            "is_transient" => {
180                fields.push("    pub is_transient: bool,".to_string());
181                methods.push(
182                    concat!(
183                        "    /// Returns `true` if the error is transient and a retry may succeed.\n",
184                        "    pub fn is_transient(&self) -> bool {\n",
185                        "        self.is_transient\n",
186                        "    }",
187                    )
188                    .to_string(),
189                );
190                ctor_assignments.push("        is_transient: e.is_transient(),".to_string());
191            }
192            "error_type" => {
193                fields.push("    pub error_type: String,".to_string());
194                methods.push(
195                    concat!(
196                        "    /// Machine-readable error category string for matching and logging.\n",
197                        "    pub fn error_type(&self) -> String {\n",
198                        "        self.error_type.clone()\n",
199                        "    }",
200                    )
201                    .to_string(),
202                );
203                ctor_assignments.push("        error_type: e.error_type().to_string(),".to_string());
204            }
205            other => {
206                methods.push(format!("    // Not emitted: method for `{other}` on `{struct_name}`"));
207            }
208        }
209    }
210
211    let struct_def = format!("#[php_class]\npub struct {struct_name} {{\n{}\n}}", fields.join("\n"));
212
213    let from_fn = format!(
214        "#[allow(dead_code)]\nfn {snake_name}_info(e: &{rust_path}) -> {struct_name} {{\n    {struct_name} {{\n{}\n    }}\n}}",
215        ctor_assignments.join("\n"),
216        snake_name = to_snake_case(&error.name),
217    );
218
219    let impl_block = format!("#[php_impl]\nimpl {struct_name} {{\n{}\n}}", methods.join("\n\n"));
220
221    format!("{struct_def}\n\n{from_fn}\n\n{impl_block}")
222}
223
224// ---------------------------------------------------------------------------
225// Magnus (Ruby) error generation
226// ---------------------------------------------------------------------------
227
228/// Generate a converter function that maps a core error to `magnus::Error`.
229pub fn gen_magnus_error_converter(error: &ErrorDef, core_import: &str) -> String {
230    let rust_path = if error.rust_path.is_empty() {
231        format!("{core_import}::{}", error.name)
232    } else {
233        error.rust_path.replace('-', "_")
234    };
235
236    let fn_name = format!("{}_to_magnus_err", to_snake_case(&error.name));
237
238    crate::codegen::template_env::render(
239        "error_gen/magnus_error_converter.jinja",
240        minijinja::context! {
241            rust_path => rust_path.as_str(),
242            fn_name => fn_name.as_str(),
243        },
244    )
245}
246
247/// Return the Magnus converter function name for a given error type.
248pub fn magnus_converter_fn_name(error: &ErrorDef) -> String {
249    format!("{}_to_magnus_err", to_snake_case(&error.name))
250}
251
252// ---------------------------------------------------------------------------
253// Rustler (Elixir) error generation
254// ---------------------------------------------------------------------------
255
256/// Generate a converter function that maps a core error to a Rustler error tuple `{:error, reason}`.
257pub fn gen_rustler_error_converter(error: &ErrorDef, core_import: &str) -> String {
258    let rust_path = if error.rust_path.is_empty() {
259        format!("{core_import}::{}", error.name)
260    } else {
261        error.rust_path.replace('-', "_")
262    };
263
264    let fn_name = format!("{}_to_rustler_err", to_snake_case(&error.name));
265
266    crate::codegen::template_env::render(
267        "error_gen/rustler_error_converter.jinja",
268        minijinja::context! {
269            rust_path => rust_path.as_str(),
270            fn_name => fn_name.as_str(),
271        },
272    )
273}
274
275/// Return the Rustler converter function name for a given error type.
276pub fn rustler_converter_fn_name(error: &ErrorDef) -> String {
277    format!("{}_to_rustler_err", to_snake_case(&error.name))
278}
279
280// ---------------------------------------------------------------------------
281// FFI (C) error code generation
282// ---------------------------------------------------------------------------
283
284/// Generate a C enum of error codes plus an error-message function declaration.
285///
286/// Produces a `typedef enum` with `PREFIX_ERROR_NONE = 0` followed by one entry
287/// per variant, plus a function that returns the default message for a given code.
288pub fn gen_ffi_error_codes(error: &ErrorDef) -> String {
289    let prefix = to_screaming_snake(&error.name);
290    let prefix_lower = to_snake_case(&error.name);
291
292    // Pre-compute (variant_screaming, index) pairs
293    let mut variant_variants = Vec::new();
294    for (i, variant) in error.variants.iter().enumerate() {
295        let variant_screaming = to_screaming_snake(&variant.name);
296        variant_variants.push((variant_screaming, (i + 1).to_string()));
297    }
298
299    crate::codegen::template_env::render(
300        "error_gen/ffi_error_codes.jinja",
301        minijinja::context! {
302            error_name => error.name.as_str(),
303            prefix => prefix.as_str(),
304            prefix_lower => prefix_lower.as_str(),
305            variant_variants => variant_variants,
306        },
307    )
308}
309
310/// Generate `#[no_mangle] extern "C"` helper functions for the whitelisted
311/// introspection methods (`status_code`, `is_transient`, `error_type`) declared
312/// in `error.methods`.
313///
314/// Each function follows the opaque-pointer convention: accepts a
315/// `*const {rust_path}` (null-checked before dereference) and returns the
316/// method's value. For `error_type` an additional `*_error_type_free` companion
317/// is emitted so callers can release the `CString`-allocated memory.
318///
319/// Returns an empty string when `error.methods` is empty.
320pub fn gen_ffi_error_methods(error: &ErrorDef, core_import: &str, api_prefix: &str) -> String {
321    if error.methods.is_empty() {
322        return String::new();
323    }
324
325    let rust_path = if error.rust_path.is_empty() {
326        format!("{core_import}::{}", error.name)
327    } else {
328        error.rust_path.replace('-', "_")
329    };
330
331    let error_snake = to_snake_case(&error.name);
332    let mut items: Vec<String> = Vec::new();
333
334    for method in &error.methods {
335        match method.name.as_str() {
336            "status_code" => {
337                let fn_name = format!("{api_prefix}_{error_snake}_status_code");
338                items.push(format!(
339                    "/// Return the HTTP status code for the error pointed to by `err`.\n\
340                     /// Returns `0` if `err` is null.\n\
341                     #[no_mangle]\n\
342                     pub unsafe extern \"C\" fn {fn_name}(err: *const {rust_path}) -> u16 {{\n\
343                         // SAFETY: caller guarantees `err` points to a live `{rust_path}` value\n\
344                         // allocated by this library, or is null.\n\
345                         if err.is_null() {{\n\
346                             return 0;\n\
347                         }}\n\
348                         (*err).status_code()\n\
349                     }}"
350                ));
351            }
352            "is_transient" => {
353                let fn_name = format!("{api_prefix}_{error_snake}_is_transient");
354                items.push(format!(
355                    "/// Return whether the error pointed to by `err` is transient.\n\
356                     /// Returns `false` if `err` is null.\n\
357                     #[no_mangle]\n\
358                     pub unsafe extern \"C\" fn {fn_name}(err: *const {rust_path}) -> bool {{\n\
359                         // SAFETY: caller guarantees `err` points to a live `{rust_path}` value\n\
360                         // allocated by this library, or is null.\n\
361                         if err.is_null() {{\n\
362                             return false;\n\
363                         }}\n\
364                         (*err).is_transient()\n\
365                     }}"
366                ));
367            }
368            "error_type" => {
369                let fn_name = format!("{api_prefix}_{error_snake}_error_type");
370                let free_fn_name = format!("{fn_name}_free");
371                items.push(format!(
372                    "/// Return the machine-readable error category string for the error pointed\n\
373                     /// to by `err` as a heap-allocated, NUL-terminated C string.\n\
374                     /// The caller must free the returned pointer with `{free_fn_name}`.\n\
375                     /// Returns a null pointer if `err` is null.\n\
376                     #[no_mangle]\n\
377                     pub unsafe extern \"C\" fn {fn_name}(err: *const {rust_path}) -> *mut std::ffi::c_char {{\n\
378                         // SAFETY: caller guarantees `err` points to a live `{rust_path}` value\n\
379                         // allocated by this library, or is null.\n\
380                         if err.is_null() {{\n\
381                             return std::ptr::null_mut();\n\
382                         }}\n\
383                         let s = (*err).error_type();\n\
384                         // SAFETY: `error_type()` returns a `'static str` containing no NUL bytes.\n\
385                         std::ffi::CString::new(s)\n\
386                             .map(|c| c.into_raw())\n\
387                             .unwrap_or(std::ptr::null_mut())\n\
388                     }}\n\n\
389                     /// Free a string previously returned by `{fn_name}`.\n\
390                     /// Passing a null pointer is a no-op.\n\
391                     #[no_mangle]\n\
392                     pub unsafe extern \"C\" fn {free_fn_name}(ptr: *mut std::ffi::c_char) {{\n\
393                         // SAFETY: `ptr` was allocated by `CString::into_raw` inside\n\
394                         // `{fn_name}` and is now being reclaimed by the matching\n\
395                         // `CString::from_raw`.  Passing null is explicitly allowed.\n\
396                         if !ptr.is_null() {{\n\
397                             drop(std::ffi::CString::from_raw(ptr));\n\
398                         }}\n\
399                     }}"
400                ));
401            }
402            other => {
403                // Unknown whitelisted method — emit a comment so it is visible in review.
404                items.push(format!(
405                    "// Not emitted: FFI helper for method `{other}` on `{rust_path}`"
406                ));
407            }
408        }
409    }
410
411    items.join("\n\n")
412}
413
414// ---------------------------------------------------------------------------
415// Go error type generation
416// ---------------------------------------------------------------------------
417
418/// Generate Go sentinel errors and a structured error type for an `ErrorDef`.
419///
420/// `pkg_name` is the Go package name (e.g. `"samplellm"`). When the error struct
421/// name starts with the package name (case-insensitively), the package-name
422/// prefix is stripped to avoid the revive `exported` stutter lint error
423/// (e.g. `SampleLlmError` in package `samplellm` → exported as `Error`).
424use crate::core::ir::ErrorDef;
425
426use super::shared::{error_variant_wildcard_pattern, to_screaming_snake, to_snake_case};