Skip to main content

alef_codegen/conversions/
binding_to_core.rs

1use alef_core::ir::{CoreWrapper, PrimitiveType, TypeDef, TypeRef};
2use std::fmt::Write;
3
4use super::ConversionConfig;
5use super::helpers::{core_prim_str, core_type_path, is_newtype, is_tuple_type_name, 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    // When cfg-gated fields exist, ..Default::default() fills them when the feature is enabled.
19    // When disabled, all fields are already specified and the update has no effect — suppress lint.
20    if typ.has_stripped_cfg_fields {
21        writeln!(out, "#[allow(clippy::needless_update)]").ok();
22    }
23    writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
24    writeln!(out, "    fn from(val: {binding_name}) -> Self {{").ok();
25
26    // Newtype structs: generate tuple constructor Self(val._0)
27    if is_newtype(typ) {
28        let field = &typ.fields[0];
29        let inner_expr = match &field.ty {
30            TypeRef::Named(_) => "val._0.into()".to_string(),
31            TypeRef::Path => "val._0.into()".to_string(),
32            TypeRef::Duration => "std::time::Duration::from_millis(val._0)".to_string(),
33            _ => "val._0".to_string(),
34        };
35        writeln!(out, "        Self({inner_expr})").ok();
36        writeln!(out, "    }}").ok();
37        write!(out, "}}").ok();
38        return out;
39    }
40
41    // When option_duration_on_defaults is set for a has_default type, non-optional Duration
42    // fields are stored as Option<u64> in the binding struct.  We use the builder pattern
43    // so that None falls back to the core type's Default (giving the real field default,
44    // e.g. Duration::from_millis(30000)) rather than Duration::ZERO.
45    let has_optionalized_duration = config.option_duration_on_defaults
46        && typ.has_default
47        && typ
48            .fields
49            .iter()
50            .any(|f| !f.optional && matches!(f.ty, TypeRef::Duration));
51
52    if has_optionalized_duration {
53        // Builder pattern: start from core default, override explicitly-set fields.
54        writeln!(out, "        let mut __result = {core_path}::default();").ok();
55        let optionalized = config.optionalize_defaults && typ.has_default;
56        for field in &typ.fields {
57            if field.sanitized {
58                // sanitized fields keep the default value — skip
59                continue;
60            }
61            // Duration field stored as Option<u64/i64>: only override when Some
62            if !field.optional && matches!(field.ty, TypeRef::Duration) {
63                let cast = if config.cast_large_ints_to_i64 { " as u64" } else { "" };
64                writeln!(
65                    out,
66                    "        if let Some(__v) = val.{} {{ __result.{} = std::time::Duration::from_millis(__v{cast}); }}",
67                    field.name, field.name
68                )
69                .ok();
70                continue;
71            }
72            let conversion = if optionalized && !field.optional {
73                gen_optionalized_field_to_core(&field.name, &field.ty, config)
74            } else {
75                field_conversion_to_core_cfg(&field.name, &field.ty, field.optional, config)
76            };
77            // Strip the "name: " prefix to get just the expression, then assign
78            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
79                writeln!(out, "        __result.{} = {};", field.name, expr).ok();
80            }
81        }
82        writeln!(out, "        __result").ok();
83        writeln!(out, "    }}").ok();
84        write!(out, "}}").ok();
85        return out;
86    }
87
88    writeln!(out, "        Self {{").ok();
89    let optionalized = config.optionalize_defaults && typ.has_default;
90    for field in &typ.fields {
91        let conversion = if field.sanitized {
92            format!("{}: Default::default()", field.name)
93        } else if optionalized && !field.optional {
94            // Field was wrapped in Option<T> for JS ergonomics but core expects T.
95            // Use unwrap_or_default() for simple types, unwrap_or_default() + into for Named.
96            gen_optionalized_field_to_core(&field.name, &field.ty, config)
97        } else {
98            field_conversion_to_core_cfg(&field.name, &field.ty, field.optional, config)
99        };
100        // Newtype wrapping: when the field was resolved from a newtype (e.g. NodeIndex → u32),
101        // wrap the binding value back into the newtype for the core struct.
102        // e.g. `source: val.source` → `source: kreuzberg::NodeIndex(val.source)`
103        //      `parent: val.parent` → `parent: val.parent.map(kreuzberg::NodeIndex)`
104        //      `children: val.children` → `children: val.children.into_iter().map(kreuzberg::NodeIndex).collect()`
105        let conversion = if let Some(newtype_path) = &field.newtype_wrapper {
106            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
107                // When `optional=true` and `ty` is a plain Primitive (not TypeRef::Optional), the core
108                // field is actually `Option<NewtypeT>`, so we must use `.map(NewtypeT)` not `NewtypeT(...)`.
109                match &field.ty {
110                    TypeRef::Optional(_) => format!("{}: ({expr}).map({newtype_path})", field.name),
111                    TypeRef::Vec(_) => {
112                        format!("{}: ({expr}).into_iter().map({newtype_path}).collect()", field.name)
113                    }
114                    _ if field.optional => format!("{}: ({expr}).map({newtype_path})", field.name),
115                    _ => format!("{}: {newtype_path}({expr})", field.name),
116                }
117            } else {
118                conversion
119            }
120        } else {
121            conversion
122        };
123        // Box<T> fields: wrap the converted value in Box::new()
124        let conversion = if field.is_boxed && matches!(&field.ty, TypeRef::Named(_)) {
125            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
126                if field.optional {
127                    // Option<Box<T>> field: map inside the Option
128                    format!("{}: {}.map(Box::new)", field.name, expr)
129                } else {
130                    format!("{}: Box::new({})", field.name, expr)
131                }
132            } else {
133                conversion
134            }
135        } else {
136            conversion
137        };
138        // CoreWrapper: apply Cow/Arc/Bytes wrapping for binding→core direction
139        let conversion = apply_core_wrapper_to_core(
140            &conversion,
141            &field.name,
142            &field.core_wrapper,
143            &field.vec_inner_core_wrapper,
144            field.optional,
145        );
146        writeln!(out, "            {conversion},").ok();
147    }
148    // Use ..Default::default() to fill cfg-gated fields stripped from the IR
149    if typ.has_stripped_cfg_fields {
150        writeln!(out, "            ..Default::default()").ok();
151    }
152    writeln!(out, "        }}").ok();
153    writeln!(out, "    }}").ok();
154    write!(out, "}}").ok();
155    out
156}
157
158/// Generate field conversion for a non-optional field that was optionalized
159/// (wrapped in Option<T>) in the binding struct for JS ergonomics.
160pub(super) fn gen_optionalized_field_to_core(name: &str, ty: &TypeRef, config: &ConversionConfig) -> String {
161    match ty {
162        TypeRef::Json => {
163            format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or_default()")
164        }
165        TypeRef::Named(_) => {
166            // Named type: unwrap Option, convert via .into(), or use Default
167            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
168        }
169        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
170            format!("{name}: val.{name}.map(|v| v as f32).unwrap_or(0.0)")
171        }
172        TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => {
173            format!("{name}: val.{name}.unwrap_or(0.0)")
174        }
175        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
176            let core_ty = core_prim_str(p);
177            format!("{name}: val.{name}.map(|v| v as {core_ty}).unwrap_or_default()")
178        }
179        TypeRef::Duration if config.cast_large_ints_to_i64 => {
180            format!("{name}: val.{name}.map(|v| std::time::Duration::from_millis(v as u64)).unwrap_or_default()")
181        }
182        TypeRef::Duration => {
183            format!("{name}: val.{name}.map(std::time::Duration::from_millis).unwrap_or_default()")
184        }
185        TypeRef::Path => {
186            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
187        }
188        // Char: binding uses Option<String>, core uses char
189        TypeRef::Char => {
190            format!("{name}: val.{name}.and_then(|s| s.chars().next()).unwrap_or('*')")
191        }
192        TypeRef::Vec(inner) => match inner.as_ref() {
193            TypeRef::Json => {
194                format!(
195                    "{name}: val.{name}.map(|v| v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()).unwrap_or_default()"
196                )
197            }
198            TypeRef::Named(_) => {
199                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect()).unwrap_or_default()")
200            }
201            TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
202                let core_ty = core_prim_str(p);
203                format!(
204                    "{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect()).unwrap_or_default()"
205                )
206            }
207            _ => format!("{name}: val.{name}.unwrap_or_default()"),
208        },
209        TypeRef::Map(_, _) => {
210            // Collect to handle HashMap↔BTreeMap conversion
211            format!("{name}: val.{name}.unwrap_or_default().into_iter().collect()")
212        }
213        _ => {
214            // Simple types (primitives, String, etc): unwrap_or_default()
215            format!("{name}: val.{name}.unwrap_or_default()")
216        }
217    }
218}
219
220/// Determine the field conversion expression for binding -> core.
221pub fn field_conversion_to_core(name: &str, ty: &TypeRef, optional: bool) -> String {
222    match ty {
223        // Primitives, String, Bytes, Unit -- direct assignment
224        TypeRef::Primitive(_) | TypeRef::String | TypeRef::Bytes | TypeRef::Unit => {
225            format!("{name}: val.{name}")
226        }
227        // Json: binding uses String, core uses serde_json::Value — parse or default
228        TypeRef::Json => {
229            if optional {
230                format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())")
231            } else {
232                format!("{name}: serde_json::from_str(&val.{name}).unwrap_or_default()")
233            }
234        }
235        // Char: binding uses String, core uses char — convert first character
236        TypeRef::Char => {
237            if optional {
238                format!("{name}: val.{name}.and_then(|s| s.chars().next())")
239            } else {
240                format!("{name}: val.{name}.chars().next().unwrap_or('*')")
241            }
242        }
243        // Duration: binding uses u64 (millis), core uses std::time::Duration
244        TypeRef::Duration => {
245            if optional {
246                format!("{name}: val.{name}.map(std::time::Duration::from_millis)")
247            } else {
248                format!("{name}: std::time::Duration::from_millis(val.{name})")
249            }
250        }
251        // Path needs .into() — binding uses String, core uses PathBuf
252        TypeRef::Path => {
253            if optional {
254                format!("{name}: val.{name}.map(Into::into)")
255            } else {
256                format!("{name}: val.{name}.into()")
257            }
258        }
259        // Named type -- needs .into() to convert between binding and core types
260        // Tuple types (e.g., "(String, String)") are passthrough — no conversion needed
261        TypeRef::Named(type_name) if is_tuple_type_name(type_name) => {
262            format!("{name}: val.{name}")
263        }
264        TypeRef::Named(_) => {
265            if optional {
266                format!("{name}: val.{name}.map(Into::into)")
267            } else {
268                format!("{name}: val.{name}.into()")
269            }
270        }
271        // Optional with inner
272        TypeRef::Optional(inner) => match inner.as_ref() {
273            TypeRef::Json => format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())"),
274            TypeRef::Named(_) | TypeRef::Path => format!("{name}: val.{name}.map(Into::into)"),
275            TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_)) => {
276                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
277            }
278            _ => format!("{name}: val.{name}"),
279        },
280        // Vec of named or Json types -- map each element
281        TypeRef::Vec(inner) => match inner.as_ref() {
282            TypeRef::Json => {
283                if optional {
284                    format!(
285                        "{name}: val.{name}.map(|v| v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect())"
286                    )
287                } else {
288                    format!("{name}: val.{name}.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()")
289                }
290            }
291            // Vec<(T1, T2)> — tuples are passthrough
292            TypeRef::Named(type_name) if is_tuple_type_name(type_name) => {
293                format!("{name}: val.{name}")
294            }
295            TypeRef::Named(_) => {
296                if optional {
297                    format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
298                } else {
299                    format!("{name}: val.{name}.into_iter().map(Into::into).collect()")
300                }
301            }
302            _ => format!("{name}: val.{name}"),
303        },
304        // Map -- collect to handle HashMap↔BTreeMap conversion;
305        // additionally convert Named keys/values via Into, Json values via serde.
306        TypeRef::Map(k, v) => {
307            let has_named_key = matches!(k.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
308            let has_named_val = matches!(v.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
309            let has_json_val = matches!(v.as_ref(), TypeRef::Json);
310            let has_json_key = matches!(k.as_ref(), TypeRef::Json);
311            if has_json_val || has_json_key || has_named_key || has_named_val {
312                let k_expr = if has_json_key {
313                    "serde_json::from_str(&k).unwrap_or(serde_json::Value::String(k))"
314                } else if has_named_key {
315                    "k.into()"
316                } else {
317                    "k"
318                };
319                let v_expr = if has_json_val {
320                    "serde_json::from_str(&v).unwrap_or(serde_json::Value::String(v))"
321                } else if has_named_val {
322                    "v.into()"
323                } else {
324                    "v"
325                };
326                if optional {
327                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect())")
328                } else {
329                    format!("{name}: val.{name}.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect()")
330                }
331            } else {
332                // No conversion needed — just collect for potential HashMap↔BTreeMap type change
333                if optional {
334                    format!("{name}: val.{name}.map(|m| m.into_iter().collect())")
335                } else {
336                    format!("{name}: val.{name}.into_iter().collect()")
337                }
338            }
339        }
340    }
341}
342
343/// Binding→core field conversion with backend-specific config (i64 casts, etc.).
344pub fn field_conversion_to_core_cfg(name: &str, ty: &TypeRef, optional: bool, config: &ConversionConfig) -> String {
345    // WASM JsValue: use serde_wasm_bindgen for Map, nested Vec, and Vec<Json> types
346    if config.map_uses_jsvalue {
347        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
348        let is_vec_json = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json));
349        let is_map = matches!(ty, TypeRef::Map(_, _));
350        if is_nested_vec || is_map || is_vec_json {
351            if optional {
352                return format!(
353                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
354                );
355            }
356            return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
357        }
358        if let TypeRef::Optional(inner) = ty {
359            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
360            let is_inner_vec_json = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Json));
361            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
362            if is_inner_nested || is_inner_map || is_inner_vec_json {
363                return format!(
364                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
365                );
366            }
367        }
368    }
369
370    // Json→String binding→core: use Default::default() (lossy — can't parse String back)
371    if config.json_to_string && matches!(ty, TypeRef::Json) {
372        return format!("{name}: Default::default()");
373    }
374    // Json→JsValue binding→core: use serde_wasm_bindgen to convert (WASM)
375    if config.map_uses_jsvalue && matches!(ty, TypeRef::Json) {
376        if optional {
377            return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())");
378        }
379        return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
380    }
381    if !config.cast_large_ints_to_i64 && !config.cast_f32_to_f64 && !config.json_to_string {
382        return field_conversion_to_core(name, ty, optional);
383    }
384    // Cast mode: handle primitives and Duration differently
385    match ty {
386        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
387            let core_ty = core_prim_str(p);
388            if optional {
389                format!("{name}: val.{name}.map(|v| v as {core_ty})")
390            } else {
391                format!("{name}: val.{name} as {core_ty}")
392            }
393        }
394        // f64→f32 cast (NAPI binding f64 → core f32)
395        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
396            if optional {
397                format!("{name}: val.{name}.map(|v| v as f32)")
398            } else {
399                format!("{name}: val.{name} as f32")
400            }
401        }
402        TypeRef::Duration if config.cast_large_ints_to_i64 => {
403            if optional {
404                format!("{name}: val.{name}.map(|v| std::time::Duration::from_millis(v as u64))")
405            } else {
406                format!("{name}: std::time::Duration::from_millis(val.{name} as u64)")
407            }
408        }
409        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) => {
410            if let TypeRef::Primitive(p) = inner.as_ref() {
411                let core_ty = core_prim_str(p);
412                format!("{name}: val.{name}.map(|v| v as {core_ty})")
413            } else {
414                field_conversion_to_core(name, ty, optional)
415            }
416        }
417        // Vec<u64/usize/isize> needs element-wise i64→core casting
418        TypeRef::Vec(inner)
419            if config.cast_large_ints_to_i64
420                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
421        {
422            if let TypeRef::Primitive(p) = inner.as_ref() {
423                let core_ty = core_prim_str(p);
424                if optional {
425                    format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
426                } else {
427                    format!("{name}: val.{name}.into_iter().map(|v| v as {core_ty}).collect()")
428                }
429            } else {
430                field_conversion_to_core(name, ty, optional)
431            }
432        }
433        // Vec<f32> needs element-wise cast when f32→f64 mapping is active (NAPI)
434        TypeRef::Vec(inner)
435            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
436        {
437            if optional {
438                format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
439            } else {
440                format!("{name}: val.{name}.into_iter().map(|v| v as f32).collect()")
441            }
442        }
443        // Optional(Vec(f32)) needs element-wise cast (NAPI only)
444        TypeRef::Optional(inner)
445            if config.cast_f32_to_f64
446                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
447        {
448            format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
449        }
450        // Fall through to default for everything else
451        _ => field_conversion_to_core(name, ty, optional),
452    }
453}
454
455/// Apply CoreWrapper transformations to a binding→core conversion expression.
456/// Wraps the value expression with Arc::new(), .into() for Cow, etc.
457fn apply_core_wrapper_to_core(
458    conversion: &str,
459    name: &str,
460    core_wrapper: &CoreWrapper,
461    vec_inner_core_wrapper: &CoreWrapper,
462    optional: bool,
463) -> String {
464    // Handle Vec<Arc<T>>: replace .map(Into::into) with .map(|v| std::sync::Arc::new(v.into()))
465    if *vec_inner_core_wrapper == CoreWrapper::Arc {
466        return conversion
467            .replace(
468                ".map(Into::into).collect()",
469                ".map(|v| std::sync::Arc::new(v.into())).collect()",
470            )
471            .replace(
472                "map(|v| v.into_iter().map(Into::into)",
473                "map(|v| v.into_iter().map(|v| std::sync::Arc::new(v.into()))",
474            );
475    }
476
477    match core_wrapper {
478        CoreWrapper::None => conversion.to_string(),
479        CoreWrapper::Cow => {
480            // Cow<str>: binding String → core Cow via .into()
481            // The field_conversion already emits "name: val.name" for strings,
482            // we need to add .into() to convert String → Cow<'static, str>
483            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
484                if optional {
485                    format!("{name}: {expr}.map(Into::into)")
486                } else if expr == format!("val.{name}") {
487                    format!("{name}: val.{name}.into()")
488                } else if expr == "Default::default()" {
489                    // Sanitized field: Default::default() already resolves to the correct core type
490                    // (e.g. Cow<'static, str> — adding .into() breaks type inference).
491                    conversion.to_string()
492                } else {
493                    format!("{name}: ({expr}).into()")
494                }
495            } else {
496                conversion.to_string()
497            }
498        }
499        CoreWrapper::Arc => {
500            // Arc<T>: wrap with Arc::new()
501            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
502                if expr == "Default::default()" {
503                    // Sanitized field: Default::default() resolves to the correct core type;
504                    // wrapping in Arc::new() would change the type.
505                    conversion.to_string()
506                } else if optional {
507                    format!("{name}: {expr}.map(|v| std::sync::Arc::new(v))")
508                } else {
509                    format!("{name}: std::sync::Arc::new({expr})")
510                }
511            } else {
512                conversion.to_string()
513            }
514        }
515        CoreWrapper::Bytes => {
516            // Bytes: binding Vec<u8> → core Bytes via .into()
517            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
518                if optional {
519                    format!("{name}: {expr}.map(Into::into)")
520                } else if expr == format!("val.{name}") {
521                    format!("{name}: val.{name}.into()")
522                } else if expr == "Default::default()" {
523                    // Sanitized field: Default::default() already resolves to the correct core type
524                    // (e.g. bytes::Bytes — adding .into() breaks type inference).
525                    conversion.to_string()
526                } else {
527                    format!("{name}: ({expr}).into()")
528                }
529            } else {
530                conversion.to_string()
531            }
532        }
533    }
534}