Skip to main content

alef_codegen/conversions/
binding_to_core.rs

1use alef_core::ir::{PrimitiveType, TypeDef, TypeRef};
2use std::fmt::Write;
3
4use super::ConversionConfig;
5use super::helpers::{core_prim_str, core_type_path, needs_i64_cast};
6
7/// Generate `impl From<BindingType> for core::Type` (binding -> core).
8/// Sanitized fields use `Default::default()` (lossy but functional).
9pub fn gen_from_binding_to_core(typ: &TypeDef, core_import: &str) -> String {
10    gen_from_binding_to_core_cfg(typ, core_import, &ConversionConfig::default())
11}
12
13/// Generate `impl From<BindingType> for core::Type` with backend-specific config.
14pub fn gen_from_binding_to_core_cfg(typ: &TypeDef, core_import: &str, config: &ConversionConfig) -> String {
15    let core_path = core_type_path(typ, core_import);
16    let binding_name = format!("{}{}", config.type_name_prefix, typ.name);
17    let mut out = String::with_capacity(256);
18    writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
19    writeln!(out, "    fn from(val: {binding_name}) -> Self {{").ok();
20    writeln!(out, "        Self {{").ok();
21    let optionalized = config.optionalize_defaults && typ.has_default;
22    for field in &typ.fields {
23        let conversion = if field.sanitized {
24            format!("{}: Default::default()", field.name)
25        } else if optionalized && !field.optional {
26            // Field was wrapped in Option<T> for JS ergonomics but core expects T.
27            // Use unwrap_or_default() for simple types, unwrap_or_default() + into for Named.
28            gen_optionalized_field_to_core(&field.name, &field.ty, config)
29        } else {
30            field_conversion_to_core_cfg(&field.name, &field.ty, field.optional, config)
31        };
32        // Box<T> fields: wrap the converted value in Box::new()
33        let conversion = if field.is_boxed && matches!(&field.ty, TypeRef::Named(_)) {
34            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
35                format!("{}: Box::new({})", field.name, expr)
36            } else {
37                conversion
38            }
39        } else {
40            conversion
41        };
42        writeln!(out, "            {conversion},").ok();
43    }
44    // Use ..Default::default() to fill cfg-gated fields stripped from the IR
45    if typ.has_stripped_cfg_fields {
46        writeln!(out, "            ..Default::default()").ok();
47    }
48    writeln!(out, "        }}").ok();
49    writeln!(out, "    }}").ok();
50    write!(out, "}}").ok();
51    out
52}
53
54/// Generate field conversion for a non-optional field that was optionalized
55/// (wrapped in Option<T>) in the binding struct for JS ergonomics.
56pub(super) fn gen_optionalized_field_to_core(name: &str, ty: &TypeRef, config: &ConversionConfig) -> String {
57    match ty {
58        TypeRef::Named(_) => {
59            // Named type: unwrap Option, convert via .into(), or use Default
60            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
61        }
62        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
63            let core_ty = core_prim_str(p);
64            format!("{name}: val.{name}.map(|v| v as {core_ty}).unwrap_or_default()")
65        }
66        TypeRef::Duration if config.cast_large_ints_to_i64 => {
67            format!("{name}: val.{name}.map(|v| std::time::Duration::from_secs(v as u64)).unwrap_or_default()")
68        }
69        TypeRef::Duration => {
70            format!("{name}: val.{name}.map(std::time::Duration::from_secs).unwrap_or_default()")
71        }
72        TypeRef::Path => {
73            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
74        }
75        // Char: binding uses Option<String>, core uses char
76        TypeRef::Char => {
77            format!("{name}: val.{name}.and_then(|s| s.chars().next()).unwrap_or('*')")
78        }
79        TypeRef::Vec(inner) => match inner.as_ref() {
80            TypeRef::Named(_) => {
81                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect()).unwrap_or_default()")
82            }
83            TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
84                let core_ty = core_prim_str(p);
85                format!(
86                    "{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect()).unwrap_or_default()"
87                )
88            }
89            _ => format!("{name}: val.{name}.unwrap_or_default()"),
90        },
91        TypeRef::Map(_, _) => {
92            // Collect to handle HashMap↔BTreeMap conversion
93            format!("{name}: val.{name}.unwrap_or_default().into_iter().collect()")
94        }
95        _ => {
96            // Simple types (primitives, String, etc): unwrap_or_default()
97            format!("{name}: val.{name}.unwrap_or_default()")
98        }
99    }
100}
101
102/// Determine the field conversion expression for binding -> core.
103pub fn field_conversion_to_core(name: &str, ty: &TypeRef, optional: bool) -> String {
104    match ty {
105        // Primitives, String, Bytes, Unit, Json -- direct assignment
106        TypeRef::Primitive(_) | TypeRef::String | TypeRef::Bytes | TypeRef::Unit | TypeRef::Json => {
107            format!("{name}: val.{name}")
108        }
109        // Char: binding uses String, core uses char — convert first character
110        TypeRef::Char => {
111            if optional {
112                format!("{name}: val.{name}.and_then(|s| s.chars().next())")
113            } else {
114                format!("{name}: val.{name}.chars().next().unwrap_or('*')")
115            }
116        }
117        // Duration: binding uses u64 (secs), core uses std::time::Duration
118        TypeRef::Duration => {
119            if optional {
120                format!("{name}: val.{name}.map(std::time::Duration::from_secs)")
121            } else {
122                format!("{name}: std::time::Duration::from_secs(val.{name})")
123            }
124        }
125        // Path needs .into() — binding uses String, core uses PathBuf
126        TypeRef::Path => {
127            if optional {
128                format!("{name}: val.{name}.map(Into::into)")
129            } else {
130                format!("{name}: val.{name}.into()")
131            }
132        }
133        // Named type -- needs .into() to convert between binding and core types
134        TypeRef::Named(_) => {
135            if optional {
136                format!("{name}: val.{name}.map(Into::into)")
137            } else {
138                format!("{name}: val.{name}.into()")
139            }
140        }
141        // Optional with inner
142        TypeRef::Optional(inner) => match inner.as_ref() {
143            TypeRef::Named(_) | TypeRef::Path => format!("{name}: val.{name}.map(Into::into)"),
144            TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_)) => {
145                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
146            }
147            _ => format!("{name}: val.{name}"),
148        },
149        // Vec of named types -- map each element
150        TypeRef::Vec(inner) => match inner.as_ref() {
151            TypeRef::Named(_) => {
152                if optional {
153                    format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
154                } else {
155                    format!("{name}: val.{name}.into_iter().map(Into::into).collect()")
156                }
157            }
158            _ => format!("{name}: val.{name}"),
159        },
160        // Map -- always collect to handle HashMap↔BTreeMap conversion;
161        // additionally convert Named keys/values via Into.
162        TypeRef::Map(k, v) => {
163            let has_named_key = matches!(k.as_ref(), TypeRef::Named(_));
164            let has_named_val = matches!(v.as_ref(), TypeRef::Named(_));
165            let k_expr = if has_named_key { "k.into()" } else { "k" };
166            let v_expr = if has_named_val { "v.into()" } else { "v" };
167            if optional {
168                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect())")
169            } else {
170                format!("{name}: val.{name}.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect()")
171            }
172        }
173    }
174}
175
176/// Binding→core field conversion with backend-specific config (i64 casts, etc.).
177pub fn field_conversion_to_core_cfg(name: &str, ty: &TypeRef, optional: bool, config: &ConversionConfig) -> String {
178    // WASM JsValue: use serde_wasm_bindgen for Map and nested Vec types
179    if config.map_uses_jsvalue {
180        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
181        let is_map = matches!(ty, TypeRef::Map(_, _));
182        if is_nested_vec || is_map {
183            if optional {
184                return format!(
185                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
186                );
187            }
188            return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
189        }
190        if let TypeRef::Optional(inner) = ty {
191            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
192            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
193            if is_inner_nested || is_inner_map {
194                return format!(
195                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
196                );
197            }
198        }
199    }
200
201    // Json→String binding→core: use Default::default() (lossy — can't parse String back)
202    if config.json_to_string && matches!(ty, TypeRef::Json) {
203        return format!("{name}: Default::default()");
204    }
205    // Json→JsValue binding→core: use serde_wasm_bindgen to convert (WASM)
206    if config.map_uses_jsvalue && matches!(ty, TypeRef::Json) {
207        if optional {
208            return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())");
209        }
210        return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
211    }
212    if !config.cast_large_ints_to_i64 && !config.cast_f32_to_f64 && !config.json_to_string {
213        return field_conversion_to_core(name, ty, optional);
214    }
215    // Cast mode: handle primitives and Duration differently
216    match ty {
217        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
218            let core_ty = core_prim_str(p);
219            if optional {
220                format!("{name}: val.{name}.map(|v| v as {core_ty})")
221            } else {
222                format!("{name}: val.{name} as {core_ty}")
223            }
224        }
225        // f64→f32 cast (NAPI binding f64 → core f32)
226        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
227            if optional {
228                format!("{name}: val.{name}.map(|v| v as f32)")
229            } else {
230                format!("{name}: val.{name} as f32")
231            }
232        }
233        TypeRef::Duration if config.cast_large_ints_to_i64 => {
234            if optional {
235                format!("{name}: val.{name}.map(|v| std::time::Duration::from_secs(v as u64))")
236            } else {
237                format!("{name}: std::time::Duration::from_secs(val.{name} as u64)")
238            }
239        }
240        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) => {
241            if let TypeRef::Primitive(p) = inner.as_ref() {
242                let core_ty = core_prim_str(p);
243                format!("{name}: val.{name}.map(|v| v as {core_ty})")
244            } else {
245                field_conversion_to_core(name, ty, optional)
246            }
247        }
248        // Vec<u64/usize/isize> needs element-wise i64→core casting
249        TypeRef::Vec(inner)
250            if config.cast_large_ints_to_i64
251                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
252        {
253            if let TypeRef::Primitive(p) = inner.as_ref() {
254                let core_ty = core_prim_str(p);
255                if optional {
256                    format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
257                } else {
258                    format!("{name}: val.{name}.into_iter().map(|v| v as {core_ty}).collect()")
259                }
260            } else {
261                field_conversion_to_core(name, ty, optional)
262            }
263        }
264        // Vec<f32> needs element-wise cast when f32→f64 mapping is active (NAPI)
265        TypeRef::Vec(inner)
266            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
267        {
268            if optional {
269                format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
270            } else {
271                format!("{name}: val.{name}.into_iter().map(|v| v as f32).collect()")
272            }
273        }
274        // Optional(Vec(f32)) needs element-wise cast (NAPI only)
275        TypeRef::Optional(inner)
276            if config.cast_f32_to_f64
277                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
278        {
279            format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
280        }
281        // Fall through to default for everything else
282        _ => field_conversion_to_core(name, ty, optional),
283    }
284}