Skip to main content

alef_codegen/generators/
enums.rs

1use crate::generators::RustBindingConfig;
2use alef_core::ir::EnumDef;
3use std::fmt::Write;
4
5/// Returns true if any variant of the enum has data fields.
6/// These enums cannot be represented as flat integer enums in bindings.
7pub fn enum_has_data_variants(enum_def: &EnumDef) -> bool {
8    enum_def.variants.iter().any(|v| !v.fields.is_empty())
9}
10
11/// Returns true if any variant of the enum has a sanitized field.
12///
13/// A sanitized field means the extractor could not resolve the field's concrete type
14/// (e.g. a `dyn Stream` or another unsized / non-serde type). When this is true the
15/// core enum does NOT implement `Serialize`, `Deserialize`, or `Default`, so the
16/// binding must not try to forward those impls.
17fn enum_has_sanitized_fields(enum_def: &EnumDef) -> bool {
18    enum_def.variants.iter().any(|v| v.fields.iter().any(|f| f.sanitized))
19}
20
21/// Generate a PyO3 data enum as a `#[pyclass]` struct wrapping the core type.
22///
23/// Data enums (tagged unions like `AuthConfig`) can't be flat int enums in PyO3.
24/// Instead, generate a frozen struct with `inner` that accepts a Python dict,
25/// serializes it to JSON, and deserializes into the core Rust type via serde.
26///
27/// When any variant field is sanitized (its type could not be resolved — e.g. contains
28/// `dyn Stream + Send` which is not `Serialize`/`Deserialize`/`Default`), the serde-
29/// based constructor and trait impls are omitted. The constructor body uses `todo!()`
30/// so the generated code still compiles while making it clear the conversion is not
31/// available at runtime.
32pub fn gen_pyo3_data_enum(enum_def: &EnumDef, core_import: &str) -> String {
33    let name = &enum_def.name;
34    let core_path = crate::conversions::core_enum_path(enum_def, core_import);
35    let has_sanitized = enum_has_sanitized_fields(enum_def);
36    let mut out = String::with_capacity(512);
37
38    writeln!(out, "#[derive(Clone)]").ok();
39    writeln!(out, "#[pyclass(frozen)]").ok();
40    writeln!(out, "pub struct {name} {{").ok();
41    writeln!(out, "    pub(crate) inner: {core_path},").ok();
42    writeln!(out, "}}").ok();
43    writeln!(out).ok();
44
45    writeln!(out, "#[pymethods]").ok();
46    writeln!(out, "impl {name} {{").ok();
47    if has_sanitized {
48        // The core type cannot be serde round-tripped (contains non-Serialize variants).
49        // Omit the #[new] constructor entirely — the type is still useful as a return value
50        // from Rust (passed back via From impls), but cannot be constructed from Python.
51        writeln!(out, "}}").ok();
52    } else {
53        writeln!(out, "    #[new]").ok();
54        writeln!(
55            out,
56            "    fn new(py: Python<'_>, value: &Bound<'_, pyo3::types::PyDict>) -> PyResult<Self> {{"
57        )
58        .ok();
59        writeln!(out, "        let json_mod = py.import(\"json\")?;").ok();
60        writeln!(
61            out,
62            "        let json_str: String = json_mod.call_method1(\"dumps\", (value,))?.extract()?;"
63        )
64        .ok();
65        writeln!(out, "        let inner: {core_path} = serde_json::from_str(&json_str)").ok();
66        writeln!(
67            out,
68            "            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!(\"Invalid {name}: {{e}}\")))?;"
69        )
70        .ok();
71        writeln!(out, "        Ok(Self {{ inner }})").ok();
72        writeln!(out, "    }}").ok();
73        writeln!(out, "}}").ok();
74    }
75    writeln!(out).ok();
76
77    // From binding → core
78    writeln!(out, "impl From<{name}> for {core_path} {{").ok();
79    writeln!(out, "    fn from(val: {name}) -> Self {{ val.inner }}").ok();
80    writeln!(out, "}}").ok();
81    writeln!(out).ok();
82
83    // From core → binding
84    writeln!(out, "impl From<{core_path}> for {name} {{").ok();
85    writeln!(out, "    fn from(val: {core_path}) -> Self {{ Self {{ inner: val }} }}").ok();
86    writeln!(out, "}}").ok();
87
88    if !has_sanitized {
89        writeln!(out).ok();
90
91        // Serialize: forward to inner so parent structs that derive serde::Serialize compile.
92        writeln!(out, "impl serde::Serialize for {name} {{").ok();
93        writeln!(
94            out,
95            "    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {{"
96        )
97        .ok();
98        writeln!(out, "        self.inner.serialize(serializer)").ok();
99        writeln!(out, "    }}").ok();
100        writeln!(out, "}}").ok();
101        writeln!(out).ok();
102
103        // Default: forward to inner's Default so parent structs that derive Default compile.
104        writeln!(out, "impl Default for {name} {{").ok();
105        writeln!(
106            out,
107            "    fn default() -> Self {{ Self {{ inner: Default::default() }} }}"
108        )
109        .ok();
110        writeln!(out, "}}").ok();
111        writeln!(out).ok();
112
113        // Deserialize: forward to inner so parent structs that derive serde::Deserialize compile.
114        writeln!(out, "impl<'de> serde::Deserialize<'de> for {name} {{").ok();
115        writeln!(
116            out,
117            "    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {{"
118        )
119        .ok();
120        writeln!(out, "        let inner = {core_path}::deserialize(deserializer)?;").ok();
121        writeln!(out, "        Ok(Self {{ inner }})").ok();
122        writeln!(out, "    }}").ok();
123        writeln!(out, "}}").ok();
124    }
125
126    out
127}
128
129/// Python keywords and builtins that cannot be used as variant identifiers in PyO3 enums.
130/// When a variant name matches one of these, a `#[pyo3(name = "...")]` rename attribute
131/// is emitted so the Rust identifier remains unchanged while Python sees a safe name.
132const PYTHON_KEYWORDS: &[&str] = &[
133    "None", "True", "False", "from", "import", "class", "def", "return", "yield", "pass", "break", "continue", "and",
134    "or", "not", "is", "in", "if", "else", "elif", "for", "while", "with", "as", "try", "except", "finally", "raise",
135    "del", "global", "nonlocal", "lambda", "assert", "type",
136];
137
138/// Generate an enum.
139pub fn gen_enum(enum_def: &EnumDef, cfg: &RustBindingConfig) -> String {
140    // All enums are generated as unit-variant-only in the binding layer.
141    // Data variants are flattened to unit variants; the From/Into conversions
142    // handle the lossy mapping (discarding / providing defaults for field data).
143    let mut out = String::with_capacity(512);
144    let mut derives: Vec<&str> = cfg.enum_derives.to_vec();
145    // Binding enums always derive Default, Serialize, and Deserialize.
146    // Default: enables using unwrap_or_default() in constructors.
147    // Serialize/Deserialize: required for FFI/type conversion across binding boundaries.
148    derives.push("Default");
149    derives.push("serde::Serialize");
150    derives.push("serde::Deserialize");
151    if !derives.is_empty() {
152        writeln!(out, "#[derive({})]", derives.join(", ")).ok();
153    }
154    for attr in cfg.enum_attrs {
155        writeln!(out, "#[{attr}]").ok();
156    }
157    // Detect PyO3 context so we can rename Python keyword variants via #[pyo3(name = "...")].
158    // The Rust identifier stays unchanged; only the Python-exposed attribute name gets the suffix.
159    let is_pyo3 = cfg.enum_attrs.iter().any(|a| a.contains("pyclass"));
160    writeln!(out, "pub enum {} {{", enum_def.name).ok();
161    for (idx, variant) in enum_def.variants.iter().enumerate() {
162        if is_pyo3 && PYTHON_KEYWORDS.contains(&variant.name.as_str()) {
163            writeln!(out, "    #[pyo3(name = \"{}_\")]", variant.name).ok();
164        }
165        // Mark the first variant as #[default] so derive(Default) works
166        if idx == 0 {
167            writeln!(out, "    #[default]").ok();
168        }
169        writeln!(out, "    {} = {idx},", variant.name).ok();
170    }
171    writeln!(out, "}}").ok();
172
173    out
174}