Skip to main content

alef_codegen/conversions/
binding_to_core.rs

1use alef_core::ir::{CoreWrapper, PrimitiveType, TypeDef, TypeRef};
2
3use super::ConversionConfig;
4use super::helpers::{
5    core_prim_str, core_type_path_remapped, is_newtype, is_tuple_type_name, needs_f64_cast, needs_i32_cast,
6    needs_i64_cast,
7};
8
9/// Generate `impl From<BindingType> for core::Type` (binding -> core).
10/// Sanitized fields use `Default::default()` unless the sanitizer only removed a
11/// core wrapper that can be reconstructed losslessly from the binding value.
12pub fn gen_from_binding_to_core(typ: &TypeDef, core_import: &str) -> String {
13    gen_from_binding_to_core_cfg(typ, core_import, &ConversionConfig::default())
14}
15
16/// Generate `impl From<BindingType> for core::Type` with backend-specific config.
17pub fn gen_from_binding_to_core_cfg(typ: &TypeDef, core_import: &str, config: &ConversionConfig) -> String {
18    let core_path = core_type_path_remapped(typ, core_import, config.source_crate_remaps);
19    let binding_name = format!("{}{}", config.type_name_prefix, typ.name);
20
21    // Newtype structs: generate tuple constructor Self(val._0)
22    if is_newtype(typ) {
23        let field = &typ.fields[0];
24        let newtype_inner_expr = match &field.ty {
25            TypeRef::Named(_) => "val._0.into()".to_string(),
26            TypeRef::Path => "val._0.into()".to_string(),
27            TypeRef::Duration => "std::time::Duration::from_millis(val._0)".to_string(),
28            _ => "val._0".to_string(),
29        };
30        return crate::template_env::render(
31            "conversions/binding_to_core_impl",
32            minijinja::context! {
33                core_path => core_path,
34                binding_name => binding_name,
35                is_newtype => true,
36                newtype_inner_expr => newtype_inner_expr,
37                builder_mode => false,
38                uses_builder_pattern => false,
39                has_stripped_cfg_fields => typ.has_stripped_cfg_fields,
40                statements => vec![] as Vec<String>,
41                fields => vec![] as Vec<String>,
42            },
43        );
44    }
45
46    // Determine if we're using the builder pattern
47    let uses_builder_pattern = (config.option_duration_on_defaults
48        && typ.has_default
49        && typ
50            .fields
51            .iter()
52            .any(|f| !f.optional && matches!(f.ty, TypeRef::Duration)))
53        || (config.optionalize_defaults && typ.has_default);
54
55    // When option_duration_on_defaults is set for a has_default type, non-optional Duration
56    // fields are stored as Option<u64> in the binding struct.  We use the builder pattern
57    // so that None falls back to the core type's Default (giving the real field default,
58    // e.g. Duration::from_millis(30000)) rather than Duration::ZERO.
59    let has_optionalized_duration = config.option_duration_on_defaults
60        && typ.has_default
61        && typ
62            .fields
63            .iter()
64            .any(|f| !f.optional && matches!(f.ty, TypeRef::Duration));
65
66    if has_optionalized_duration {
67        // Builder pattern: start from core default, override explicitly-set fields.
68        let optionalized = config.optionalize_defaults && typ.has_default;
69        let mut statements = Vec::new();
70        for field in &typ.fields {
71            // Skip cfg-gated fields — they don't exist in the binding struct.
72            if field.cfg.is_some() {
73                continue;
74            }
75            if field.sanitized && field.core_wrapper != CoreWrapper::Cow {
76                // sanitized fields keep the default value — skip
77                continue;
78            }
79            // Fields referencing excluded types keep their default value — skip
80            if !config.exclude_types.is_empty()
81                && super::helpers::field_references_excluded_type(&field.ty, config.exclude_types)
82            {
83                continue;
84            }
85            // Duration field stored as Option<u64/i64>: only override when Some
86            let binding_name_field = config.binding_field_name_owned(&typ.name, &field.name);
87            if !field.optional && matches!(field.ty, TypeRef::Duration) {
88                let cast = if config.cast_large_ints_to_i64 { " as u64" } else { "" };
89                statements.push(format!(
90                    "if let Some(__v) = val.{binding_name_field} {{ __result.{} = std::time::Duration::from_millis(__v{cast}); }}",
91                    field.name
92                ));
93                continue;
94            }
95            let conversion = if optionalized && !field.optional {
96                // Field was Option-wrapped in the binding for ergonomics; core expects T.
97                // Use unwrap_or_default to peel the binding-side Option.
98                gen_optionalized_field_to_core(&field.name, &field.ty, config, false)
99            } else {
100                // Genuinely-optional IR field (binding: Option<T>, core: Option<T>) or required
101                // field (`!field.optional` with optionalize_defaults=false). Both cases are
102                // handled correctly by `field_conversion_to_core_cfg`. Routing genuinely-optional
103                // fields through `gen_optionalized_field_to_core` would emit `.unwrap_or_default()`
104                // for primitives/String/Path/Duration and break the `Option<T>` destination.
105                field_conversion_to_core_cfg(&field.name, &field.ty, field.optional, config)
106            };
107            // Apply binding field name substitution for keyword-escaped fields.
108            let conversion = if binding_name_field != field.name {
109                conversion.replace(&format!("val.{}", field.name), &format!("val.{binding_name_field}"))
110            } else {
111                conversion
112            };
113            // Strip the "name: " prefix to get just the expression, then assign
114            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
115                statements.push(format!("__result.{} = {};", field.name, expr));
116            }
117        }
118
119        return crate::template_env::render(
120            "conversions/binding_to_core_impl",
121            minijinja::context! {
122                core_path => core_path,
123                binding_name => binding_name,
124                is_newtype => false,
125                newtype_inner_expr => "",
126                builder_mode => true,
127                uses_builder_pattern => uses_builder_pattern,
128                has_stripped_cfg_fields => typ.has_stripped_cfg_fields,
129                statements => statements,
130                fields => vec![] as Vec<String>,
131            },
132        );
133    }
134
135    let optionalized = config.optionalize_defaults && typ.has_default;
136
137    // Pre-compute all fields
138    let mut fields = Vec::new();
139    let mut statements = Vec::new();
140
141    for field in &typ.fields {
142        // Skip cfg-gated fields — they don't exist in the binding struct.
143        // When the binding is compiled, these fields are absent, and accessing them would fail.
144        // The ..Default::default() at the end fills in these fields when the core type is compiled
145        // with the required feature enabled.
146        if field.cfg.is_some() {
147            continue;
148        }
149        // Fields referencing excluded types don't exist in the binding struct.
150        // When the type has stripped cfg-gated fields, these fields may also be
151        // cfg-gated and absent from the core struct — skip them entirely and let
152        // ..Default::default() fill them in.
153        // Otherwise, use Default::default() to fill them in the core type.
154        // Sanitized fields also use Default::default() (lossy but functional).
155        let references_excluded = !config.exclude_types.is_empty()
156            && super::helpers::field_references_excluded_type(&field.ty, config.exclude_types);
157        if references_excluded && typ.has_stripped_cfg_fields {
158            continue;
159        }
160        if optionalized && ((field.sanitized && field.core_wrapper != CoreWrapper::Cow) || references_excluded) {
161            continue;
162        }
163        let field_was_optionalized = optionalized && !field.optional;
164        let conversion = if (field.sanitized && field.core_wrapper != CoreWrapper::Cow) || references_excluded {
165            format!("{}: Default::default()", field.name)
166        } else if field_was_optionalized {
167            // Field was wrapped in Option<T> for JS ergonomics but core expects T.
168            // Convert the supplied value as T; omitted fields keep the core type's Default value.
169            field_conversion_to_core_cfg(&field.name, &field.ty, false, config)
170        } else {
171            field_conversion_to_core_cfg(&field.name, &field.ty, field.optional, config)
172        };
173        // Newtype wrapping: when the field was resolved from a newtype (e.g. NodeIndex → u32),
174        // wrap the binding value back into the newtype for the core struct.
175        // e.g. `source: val.source` → `source: kreuzberg::NodeIndex(val.source)`
176        //      `parent: val.parent` → `parent: val.parent.map(kreuzberg::NodeIndex)`
177        //      `children: val.children` → `children: val.children.into_iter().map(kreuzberg::NodeIndex).collect()`
178        let conversion = if let Some(newtype_path) = &field.newtype_wrapper {
179            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
180                // When `optional=true` and `ty` is a plain Primitive (not TypeRef::Optional), the core
181                // field is actually `Option<NewtypeT>`, so we must use `.map(NewtypeT)` not `NewtypeT(...)`.
182                match &field.ty {
183                    TypeRef::Optional(_) => format!("{}: ({expr}).map({newtype_path})", field.name),
184                    TypeRef::Vec(_) => {
185                        // When the inner expr already ends with .collect() (e.g. because of a
186                        // primitive cast), the compiler cannot infer the intermediate Vec type
187                        // without an explicit type annotation. Use collect::<Vec<_>>() to make
188                        // the intermediate collection type unambiguous before mapping to newtype.
189                        let inner_expr = if let Some(prefix) = expr.strip_suffix(".collect()") {
190                            format!("{prefix}.collect::<Vec<_>>()")
191                        } else {
192                            expr.to_string()
193                        };
194                        format!(
195                            "{}: ({inner_expr}).into_iter().map({newtype_path}).collect()",
196                            field.name
197                        )
198                    }
199                    _ if field.optional => format!("{}: ({expr}).map({newtype_path})", field.name),
200                    _ => format!("{}: {newtype_path}({expr})", field.name),
201                }
202            } else {
203                conversion
204            }
205        } else {
206            conversion
207        };
208        // Box<T> fields: wrap the converted value in Box::new()
209        let conversion = if field.is_boxed && matches!(&field.ty, TypeRef::Named(_)) {
210            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
211                if field.optional {
212                    // Option<Box<T>> field: map inside the Option
213                    format!("{}: {}.map(Box::new)", field.name, expr)
214                } else {
215                    format!("{}: Box::new({})", field.name, expr)
216                }
217            } else {
218                conversion
219            }
220        } else {
221            conversion
222        };
223        // CoreWrapper: apply Cow/Arc/Bytes wrapping for binding→core direction.
224        //
225        // Special case: opaque Named field with CoreWrapper::Arc.
226        // The binding wrapper already holds `inner: Arc<CoreT>`, so the correct
227        // conversion is to extract `.inner` directly rather than calling `.into()`
228        // (which requires `From<BindingType> for CoreT`, a non-existent impl) and
229        // then wrapping in `Arc::new` (which would double-wrap the Arc).
230        let is_opaque_arc_field = field.core_wrapper == CoreWrapper::Arc
231            && matches!(&field.ty, TypeRef::Named(n) if config
232                .opaque_types
233                .is_some_and(|opaque| opaque.contains(n.as_str())));
234        // Opaque Named fields without CoreWrapper::Arc (e.g. visitor: Object<'static>) cannot be
235        // auto-converted via Into — the binding stores a raw JS object that needs a bridge.
236        // Emit Default::default() and let the caller (e.g. the convert function) set it separately.
237        let is_opaque_no_wrapper_field = field.core_wrapper == CoreWrapper::None
238            && matches!(&field.ty, TypeRef::Named(n) if config
239                .opaque_types
240                .is_some_and(|opaque| opaque.contains(n.as_str())));
241        let conversion = if is_opaque_arc_field {
242            if field.optional {
243                format!("{}: val.{}.map(|v| v.inner)", field.name, field.name)
244            } else {
245                format!("{}: val.{}.inner", field.name, field.name)
246            }
247        } else if is_opaque_no_wrapper_field {
248            format!("{}: Default::default()", field.name)
249        } else {
250            apply_core_wrapper_to_core(
251                &conversion,
252                &field.name,
253                &field.core_wrapper,
254                &field.vec_inner_core_wrapper,
255                field.optional,
256            )
257        };
258        // When the binding struct uses a keyword-escaped field name (e.g. `class_` for `class`),
259        // replace `val.{field.name}` access patterns in the conversion expression with
260        // `val.{binding_name}` so the generated From impl compiles.
261        let binding_name_field = config.binding_field_name_owned(&typ.name, &field.name);
262        let conversion = if binding_name_field != field.name {
263            conversion.replace(&format!("val.{}", field.name), &format!("val.{binding_name_field}"))
264        } else {
265            conversion
266        };
267        if optionalized {
268            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
269                if field_was_optionalized {
270                    statements.push(format!(
271                        "if let Some(__v) = val.{binding_name_field} {{ __result.{} = {}; }}",
272                        field.name,
273                        expr.replace(&format!("val.{binding_name_field}"), "__v")
274                    ));
275                } else {
276                    statements.push(format!("__result.{} = {};", field.name, expr));
277                }
278            }
279        } else {
280            fields.push(conversion);
281        }
282    }
283
284    // Note: ..Default::default() for cfg-gated fields is emitted by the template
285    // via the has_stripped_cfg_fields context variable — do not push it here.
286
287    crate::template_env::render(
288        "conversions/binding_to_core_impl",
289        minijinja::context! {
290            core_path => core_path,
291            binding_name => binding_name,
292            is_newtype => false,
293            newtype_inner_expr => "",
294            builder_mode => optionalized,
295            uses_builder_pattern => uses_builder_pattern,
296            has_stripped_cfg_fields => typ.has_stripped_cfg_fields,
297            statements => statements,
298            fields => fields,
299        },
300    )
301}
302
303/// Generate field conversion for a field that was optionalized (wrapped in `Option<T>`) in the
304/// binding struct for JS ergonomics (`optionalize_defaults`). When `field_is_ir_optional` is
305/// `true`, the field is genuinely `Option<T>` in the IR and the `Option` layer must be preserved
306/// in the output expression (use `.map(|m| …)` rather than `unwrap_or_default()`).
307pub(super) fn gen_optionalized_field_to_core(
308    name: &str,
309    ty: &TypeRef,
310    config: &ConversionConfig,
311    field_is_ir_optional: bool,
312) -> String {
313    match ty {
314        TypeRef::Json if config.json_as_value => {
315            format!("{name}: val.{name}.unwrap_or_default()")
316        }
317        TypeRef::Json => {
318            format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or_default()")
319        }
320        TypeRef::Named(_) => {
321            // Named type: unwrap Option, convert via .into(), or use Default
322            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
323        }
324        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
325            format!("{name}: val.{name}.map(|v| v as f32).unwrap_or(0.0)")
326        }
327        TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => {
328            format!("{name}: val.{name}.unwrap_or(0.0)")
329        }
330        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
331            let core_ty = core_prim_str(p);
332            format!("{name}: val.{name}.map(|v| v as {core_ty}).unwrap_or_default()")
333        }
334        TypeRef::Optional(inner)
335            if config.cast_large_ints_to_i64
336                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
337        {
338            if let TypeRef::Primitive(p) = inner.as_ref() {
339                let core_ty = core_prim_str(p);
340                format!("{name}: val.{name}.map(|v| v as {core_ty})")
341            } else {
342                field_conversion_to_core(name, ty, false)
343            }
344        }
345        TypeRef::Duration if config.cast_large_ints_to_i64 => {
346            format!("{name}: val.{name}.map(|v| std::time::Duration::from_millis(v as u64)).unwrap_or_default()")
347        }
348        TypeRef::Duration => {
349            format!("{name}: val.{name}.map(std::time::Duration::from_millis).unwrap_or_default()")
350        }
351        TypeRef::Path => {
352            format!("{name}: val.{name}.map(Into::into).unwrap_or_default()")
353        }
354        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Path) => {
355            // Binding has Option<String>, core has Option<PathBuf>
356            format!("{name}: val.{name}.map(|s| std::path::PathBuf::from(s))")
357        }
358        TypeRef::Optional(_) => {
359            // Field was flattened from Option<Option<T>> to Option<T> in the binding struct.
360            // Core expects Option<Option<T>>, so wrap with .map(Some) to reconstruct.
361            format!("{name}: val.{name}.map(Some)")
362        }
363        // Char: binding uses Option<String>, core uses char
364        TypeRef::Char => {
365            format!("{name}: val.{name}.and_then(|s| s.chars().next()).unwrap_or('*')")
366        }
367        TypeRef::Vec(inner) => match inner.as_ref() {
368            TypeRef::Json => {
369                format!(
370                    "{name}: val.{name}.map(|v| v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()).unwrap_or_default()"
371                )
372            }
373            TypeRef::Named(_) => {
374                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect()).unwrap_or_default()")
375            }
376            TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
377                let core_ty = core_prim_str(p);
378                format!(
379                    "{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect()).unwrap_or_default()"
380                )
381            }
382            _ => format!("{name}: val.{name}.unwrap_or_default()"),
383        },
384        TypeRef::Map(k, v) if matches!(v.as_ref(), TypeRef::Json) => {
385            // Map with Json values: binding uses HashMap<K, String>, core uses HashMap<K, serde_json::Value>.
386            // Use `k.into()` for non-Json keys so String→String is a no-op while still converting
387            // String→Cow<'_, str>/Box<str>/Arc<str> when the core type uses one of those wrappers.
388            let k_is_json = matches!(k.as_ref(), TypeRef::Json);
389            let k_expr = if k_is_json {
390                "serde_json::from_str(&k).unwrap_or_default()"
391            } else {
392                "k.into()"
393            };
394            format!(
395                "{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| ({k_expr}, serde_json::from_str(&v).unwrap_or(serde_json::json!(v)))).collect()"
396            )
397        }
398        TypeRef::Map(k, _v) if matches!(k.as_ref(), TypeRef::Json) => {
399            // Map with Json keys: binding uses HashMap<String, V>, core uses HashMap<serde_json::Value, V>
400            format!(
401                "{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (serde_json::from_str(&k).unwrap_or_default(), v)).collect()"
402            )
403        }
404        TypeRef::Map(k, v) => {
405            // Map with Named values need .into() conversion on each value.
406            let has_named_val = matches!(v.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
407            let has_named_key = matches!(k.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
408            let val_is_string_enum = matches!(v.as_ref(), TypeRef::Named(n)
409                if config.enum_string_names.as_ref().is_some_and(|names| names.contains(n)));
410            if field_is_ir_optional {
411                // Genuinely optional field: preserve the Option layer using .map(|m| …).
412                if val_is_string_enum {
413                    format!(
414                        "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, serde_json::from_str(&v).unwrap_or_default())).collect())"
415                    )
416                } else if has_named_val {
417                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())")
418                } else if has_named_key {
419                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.into(), v)).collect())")
420                } else {
421                    format!("{name}: val.{name}.map(|m| m.into_iter().collect())")
422                }
423            } else if val_is_string_enum {
424                format!(
425                    "{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (k, serde_json::from_str(&v).unwrap_or_default())).collect()"
426                )
427            } else if has_named_val {
428                format!("{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (k, v.into())).collect()")
429            } else if has_named_key {
430                format!("{name}: val.{name}.unwrap_or_default().into_iter().map(|(k, v)| (k.into(), v)).collect()")
431            } else {
432                format!("{name}: val.{name}.unwrap_or_default().into_iter().collect()")
433            }
434        }
435        _ => {
436            // Simple types (primitives, String, etc): unwrap_or_default()
437            format!("{name}: val.{name}.unwrap_or_default()")
438        }
439    }
440}
441
442/// Determine the field conversion expression for binding -> core.
443pub fn field_conversion_to_core(name: &str, ty: &TypeRef, optional: bool) -> String {
444    match ty {
445        // Primitives, String, Unit -- direct assignment
446        TypeRef::Primitive(_) | TypeRef::String | TypeRef::Unit => {
447            format!("{name}: val.{name}")
448        }
449        // Bytes: binding may use Vec<u8> or napi `Buffer`; core uses `bytes::Bytes`
450        // (or `Vec<u8>` for some targets). `.to_vec().into()` works in all cases:
451        // Buffer → Vec<u8> via `From<Buffer> for Vec<u8>`, then `Vec<u8> → Bytes`
452        // via `From<Vec<u8>> for Bytes` (or identity From for Vec<u8>→Vec<u8>).
453        TypeRef::Bytes => {
454            if optional {
455                format!("{name}: val.{name}.map(|v| v.to_vec().into())")
456            } else {
457                format!("{name}: val.{name}.to_vec().into()")
458            }
459        }
460        // Json: binding uses String, core uses serde_json::Value — parse or default
461        TypeRef::Json => {
462            if optional {
463                format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())")
464            } else {
465                format!("{name}: serde_json::from_str(&val.{name}).unwrap_or_default()")
466            }
467        }
468        // Char: binding uses String, core uses char — convert first character
469        TypeRef::Char => {
470            if optional {
471                format!("{name}: val.{name}.and_then(|s| s.chars().next())")
472            } else {
473                format!("{name}: val.{name}.chars().next().unwrap_or('*')")
474            }
475        }
476        // Duration: binding uses u64 (millis), core uses std::time::Duration
477        TypeRef::Duration => {
478            if optional {
479                format!("{name}: val.{name}.map(std::time::Duration::from_millis)")
480            } else {
481                format!("{name}: std::time::Duration::from_millis(val.{name})")
482            }
483        }
484        // Path needs .into() — binding uses String, core uses PathBuf
485        TypeRef::Path => {
486            if optional {
487                format!("{name}: val.{name}.map(Into::into)")
488            } else {
489                format!("{name}: val.{name}.into()")
490            }
491        }
492        // Named type -- needs .into() to convert between binding and core types
493        // Tuple types (e.g., "(String, String)") are passthrough — no conversion needed
494        TypeRef::Named(type_name) if is_tuple_type_name(type_name) => {
495            format!("{name}: val.{name}")
496        }
497        TypeRef::Named(_) => {
498            if optional {
499                format!("{name}: val.{name}.map(Into::into)")
500            } else {
501                format!("{name}: val.{name}.into()")
502            }
503        }
504        // Map with Json value type: binding uses HashMap<K, String>, core uses HashMap<K, Value>.
505        // Use `k.into()` for non-Json keys so String→String is a no-op while still converting
506        // String→Cow<'_, str>/Box<str>/Arc<str> when the core type uses one of those wrappers.
507        TypeRef::Map(k, v) if matches!(v.as_ref(), TypeRef::Json) => {
508            let k_expr = if matches!(k.as_ref(), TypeRef::Json) {
509                "serde_json::from_str(&k).unwrap_or_default()"
510            } else {
511                "k.into()"
512            };
513            if optional {
514                format!(
515                    "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, serde_json::from_str(&v).unwrap_or_default())).collect())"
516                )
517            } else {
518                format!(
519                    "{name}: val.{name}.into_iter().map(|(k, v)| ({k_expr}, serde_json::from_str(&v).unwrap_or_default())).collect()"
520                )
521            }
522        }
523        // Map<K, Bytes>: binding uses Vec<u8> or napi Buffer, core uses bytes::Bytes (or Vec<u8>).
524        // `.to_vec().into()` converts Buffer→Vec<u8> (napi) or is identity for Vec<u8>→Vec<u8>.
525        TypeRef::Map(_k, v) if matches!(v.as_ref(), TypeRef::Bytes) => {
526            if optional {
527                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.to_vec().into())).collect())")
528            } else {
529                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k, v.to_vec().into())).collect()")
530            }
531        }
532        // Optional with inner
533        TypeRef::Optional(inner) => match inner.as_ref() {
534            TypeRef::Json => format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())"),
535            TypeRef::Named(_) | TypeRef::Path => format!("{name}: val.{name}.map(Into::into)"),
536            TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_)) => {
537                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
538            }
539            TypeRef::Map(k, v) if matches!(v.as_ref(), TypeRef::Json) => {
540                let k_expr = if matches!(k.as_ref(), TypeRef::Json) {
541                    "serde_json::from_str(&k).unwrap_or_default()"
542                } else {
543                    "k.into()"
544                };
545                format!(
546                    "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, serde_json::from_str(&v).unwrap_or_default())).collect())"
547                )
548            }
549            _ => format!("{name}: val.{name}"),
550        },
551        // Vec of named or Json types -- map each element
552        TypeRef::Vec(inner) => match inner.as_ref() {
553            TypeRef::Json => {
554                if optional {
555                    format!(
556                        "{name}: val.{name}.map(|v| v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect())"
557                    )
558                } else {
559                    format!("{name}: val.{name}.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()")
560                }
561            }
562            // Vec<(T1, T2)> — tuples are passthrough
563            TypeRef::Named(type_name) if is_tuple_type_name(type_name) => {
564                format!("{name}: val.{name}")
565            }
566            TypeRef::Named(_) => {
567                if optional {
568                    format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
569                } else {
570                    format!("{name}: val.{name}.into_iter().map(Into::into).collect()")
571                }
572            }
573            _ => format!("{name}: val.{name}"),
574        },
575        // Map -- collect to handle HashMap↔BTreeMap conversion;
576        // additionally convert Named keys/values via Into, Json values via serde.
577        TypeRef::Map(k, v) => {
578            let has_named_key = matches!(k.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
579            let has_named_val = matches!(v.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n));
580            let has_json_val = matches!(v.as_ref(), TypeRef::Json);
581            let has_json_key = matches!(k.as_ref(), TypeRef::Json);
582            // Vec<Named> values: each vector element needs Into conversion.
583            let has_vec_named_val = matches!(v.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(n) if !is_tuple_type_name(n)));
584            // Vec<Json> values: each element needs serde deserialization.
585            let has_vec_json_val = matches!(v.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json));
586            if has_json_val || has_json_key || has_named_key || has_named_val || has_vec_named_val || has_vec_json_val {
587                // `k.into()` is a no-op for `String`→`String` and the canonical conversion for
588                // wrapped string keys (`Cow`, `Box<str>`, `Arc<str>`) which the type resolver
589                // collapses to `TypeRef::String`.
590                let k_expr = if has_json_key {
591                    "serde_json::from_str(&k).unwrap_or(serde_json::Value::String(k))"
592                } else {
593                    "k.into()"
594                };
595                let v_expr = if has_json_val {
596                    "serde_json::from_str(&v).unwrap_or(serde_json::Value::String(v))"
597                } else if has_named_val {
598                    "v.into()"
599                } else if has_vec_named_val {
600                    "v.into_iter().map(Into::into).collect()"
601                } else if has_vec_json_val {
602                    "v.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect()"
603                } else {
604                    "v"
605                };
606                if optional {
607                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect())")
608                } else {
609                    format!("{name}: val.{name}.into_iter().map(|(k, v)| ({k_expr}, {v_expr})).collect()")
610                }
611            } else {
612                // Map<String, String>: binding may have String keys/values, core may have Box<str>/Cow<str>.
613                // Emit .map(|(k, v)| (k.into(), v.into())) which is a no-op when both sides are String.
614                // This handles cases like HashMap<String, String> (binding) → HashMap<Box<str>, Box<str>> (core).
615                let is_string_map = matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String);
616                if is_string_map {
617                    if optional {
618                        format!(
619                            "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect())"
620                        )
621                    } else {
622                        format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.into(), v.into())).collect()")
623                    }
624                } else {
625                    // No conversion needed for keys/values — just collect for potential
626                    // HashMap↔BTreeMap type change. Still apply per-value .into() when the value
627                    // type is a Named wrapper that requires conversion (e.g. a binding-side newtype).
628                    if optional {
629                        if has_named_val {
630                            format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())")
631                        } else {
632                            format!("{name}: val.{name}.map(|m| m.into_iter().collect())")
633                        }
634                    } else {
635                        format!("{name}: val.{name}.into_iter().collect()")
636                    }
637                }
638            }
639        }
640    }
641}
642
643/// Binding→core field conversion with backend-specific config (i64 casts, etc.).
644pub fn field_conversion_to_core_cfg(name: &str, ty: &TypeRef, optional: bool, config: &ConversionConfig) -> String {
645    // When optional=true and ty=Optional(T), the binding field was flattened from
646    // Option<Option<T>> to Option<T>. Core expects Option<Option<T>>, so wrap with .map(Some).
647    // This applies regardless of cast config; handle before any other dispatch.
648    if optional && matches!(ty, TypeRef::Optional(_)) {
649        // Delegate to get the inner Optional(T) → Option<T> conversion (with optional=false,
650        // since the outer Option is handled by the .map(Some) we add here).
651        let inner_expr = field_conversion_to_core_cfg(name, ty, false, config);
652        // inner_expr is "name: <expr-for-Option<T>>"; wrap it with .map(Some)
653        if let Some(expr) = inner_expr.strip_prefix(&format!("{name}: ")) {
654            return format!("{name}: ({expr}).map(Some)");
655        }
656        return inner_expr;
657    }
658
659    // WASM JsValue: use serde_wasm_bindgen for Map, nested Vec, and Vec<Json> types
660    if config.map_uses_jsvalue {
661        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
662        let is_vec_json = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json));
663        let is_map = matches!(ty, TypeRef::Map(_, _));
664        if is_nested_vec || is_map || is_vec_json {
665            if optional {
666                return format!(
667                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
668                );
669            }
670            return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
671        }
672        if let TypeRef::Optional(inner) = ty {
673            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
674            let is_inner_vec_json = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Json));
675            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
676            if is_inner_nested || is_inner_map || is_inner_vec_json {
677                return format!(
678                    "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())"
679                );
680            }
681        }
682    }
683
684    // Vec<Named>→String binding→core: binding holds JSON string, core expects Vec<Named>.
685    // Only apply serde round-trip for Vec<Named> types (complex structs that can't cross FFI).
686    // Vec<String>, Vec<Primitive>, etc. stay as-is since they map directly.
687    if config.vec_named_to_string {
688        if let TypeRef::Vec(inner) = ty {
689            if matches!(inner.as_ref(), TypeRef::Named(_)) {
690                if optional {
691                    return format!("{name}: val.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())");
692                }
693                return format!("{name}: serde_json::from_str(&val.{name}).unwrap_or_default()");
694            }
695        }
696    }
697    // Map→String binding→core: use Default::default() (lossy — can't reconstruct HashMap from Debug string)
698    if config.map_as_string && matches!(ty, TypeRef::Map(_, _)) {
699        return format!("{name}: Default::default()");
700    }
701    if config.map_as_string {
702        if let TypeRef::Optional(inner) = ty {
703            if matches!(inner.as_ref(), TypeRef::Map(_, _)) {
704                return format!("{name}: Default::default()");
705            }
706        }
707    }
708    // Untagged data enum field (binding holds serde_json::Value, core holds the typed enum):
709    // convert via serde_json::from_value.  Handles direct, Optional, and Vec wrappings.
710    if let Some(untagged_names) = config.untagged_data_enum_names {
711        let direct_named = matches!(ty, TypeRef::Named(n) if untagged_names.contains(n));
712        let optional_named = matches!(ty, TypeRef::Optional(inner)
713            if matches!(inner.as_ref(), TypeRef::Named(n) if untagged_names.contains(n)));
714        let vec_named = matches!(ty, TypeRef::Vec(inner)
715            if matches!(inner.as_ref(), TypeRef::Named(n) if untagged_names.contains(n)));
716        let optional_vec_named = matches!(ty, TypeRef::Optional(outer)
717            if matches!(outer.as_ref(), TypeRef::Vec(inner)
718                if matches!(inner.as_ref(), TypeRef::Named(n) if untagged_names.contains(n))));
719        if direct_named {
720            if optional {
721                return format!("{name}: val.{name}.and_then(|v| serde_json::from_value(v).ok())");
722            }
723            return format!("{name}: serde_json::from_value(val.{name}).unwrap_or_default()");
724        }
725        if optional_named {
726            return format!("{name}: val.{name}.and_then(|v| serde_json::from_value(v).ok())");
727        }
728        if vec_named {
729            if optional {
730                return format!(
731                    "{name}: val.{name}.map(|v| v.into_iter().filter_map(|x| serde_json::from_value(x).ok()).collect())"
732                );
733            }
734            return format!("{name}: val.{name}.into_iter().filter_map(|x| serde_json::from_value(x).ok()).collect()");
735        }
736        if optional_vec_named {
737            return format!(
738                "{name}: val.{name}.map(|v| v.into_iter().filter_map(|x| serde_json::from_value(x).ok()).collect())"
739            );
740        }
741    }
742    // Json→String binding→core: use Default::default() (lossy — can't parse String back)
743    if config.json_to_string && matches!(ty, TypeRef::Json) {
744        return format!("{name}: Default::default()");
745    }
746    // Json stays as serde_json::Value: identity passthrough.
747    if config.json_as_value && matches!(ty, TypeRef::Json) {
748        return format!("{name}: val.{name}");
749    }
750    if config.json_as_value {
751        if let TypeRef::Optional(inner) = ty {
752            if matches!(inner.as_ref(), TypeRef::Json) {
753                return format!("{name}: val.{name}");
754            }
755        }
756        if let TypeRef::Vec(inner) = ty {
757            if matches!(inner.as_ref(), TypeRef::Json) {
758                if optional {
759                    return format!("{name}: val.{name}.unwrap_or_default()");
760                }
761                return format!("{name}: val.{name}");
762            }
763        }
764        if let TypeRef::Map(_k, v) = ty {
765            if matches!(v.as_ref(), TypeRef::Json) {
766                if optional {
767                    return format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.into(), v)).collect())");
768                }
769                return format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.into(), v)).collect()");
770            }
771        }
772    }
773    // Json→JsValue binding→core: use serde_wasm_bindgen to convert (WASM)
774    if config.map_uses_jsvalue && matches!(ty, TypeRef::Json) {
775        if optional {
776            return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::from_value(v.clone()).ok())");
777        }
778        return format!("{name}: serde_wasm_bindgen::from_value(val.{name}.clone()).unwrap_or_default()");
779    }
780    if !config.cast_large_ints_to_i64
781        && !config.cast_large_ints_to_f64
782        && !config.cast_uints_to_i32
783        && !config.cast_f32_to_f64
784        && !config.json_to_string
785        && !config.vec_named_to_string
786        && !config.map_as_string
787        && config.from_binding_skip_types.is_empty()
788    {
789        return field_conversion_to_core(name, ty, optional);
790    }
791    // Cast mode: handle primitives and Duration differently
792    match ty {
793        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
794            let core_ty = core_prim_str(p);
795            if optional {
796                format!("{name}: val.{name}.map(|v| v as {core_ty})")
797            } else {
798                format!("{name}: val.{name} as {core_ty}")
799            }
800        }
801        // f64→f32 cast (NAPI binding f64 → core f32)
802        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
803            if optional {
804                format!("{name}: val.{name}.map(|v| v as f32)")
805            } else {
806                format!("{name}: val.{name} as f32")
807            }
808        }
809        TypeRef::Duration if config.cast_large_ints_to_i64 => {
810            if optional {
811                format!("{name}: val.{name}.map(|v| std::time::Duration::from_millis(v as u64))")
812            } else {
813                format!("{name}: std::time::Duration::from_millis(val.{name} as u64)")
814            }
815        }
816        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) => {
817            if let TypeRef::Primitive(p) = inner.as_ref() {
818                let core_ty = core_prim_str(p);
819                format!("{name}: val.{name}.map(|v| v as {core_ty})")
820            } else {
821                field_conversion_to_core(name, ty, optional)
822            }
823        }
824        // Vec<u64/usize/isize> needs element-wise i64→core casting
825        TypeRef::Vec(inner)
826            if config.cast_large_ints_to_i64
827                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
828        {
829            if let TypeRef::Primitive(p) = inner.as_ref() {
830                let core_ty = core_prim_str(p);
831                if optional {
832                    format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
833                } else {
834                    format!("{name}: val.{name}.into_iter().map(|v| v as {core_ty}).collect()")
835                }
836            } else {
837                field_conversion_to_core(name, ty, optional)
838            }
839        }
840        // HashMap value type casting: when value type needs i64→core casting
841        TypeRef::Map(_k, v)
842            if config.cast_large_ints_to_i64 && matches!(v.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
843        {
844            if let TypeRef::Primitive(p) = v.as_ref() {
845                let core_ty = core_prim_str(p);
846                if optional {
847                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v as {core_ty})).collect())")
848                } else {
849                    format!("{name}: val.{name}.into_iter().map(|(k, v)| (k, v as {core_ty})).collect()")
850                }
851            } else {
852                field_conversion_to_core(name, ty, optional)
853            }
854        }
855        // Vec<f32> needs element-wise cast when f32→f64 mapping is active (NAPI)
856        TypeRef::Vec(inner)
857            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
858        {
859            if optional {
860                format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
861            } else {
862                format!("{name}: val.{name}.into_iter().map(|v| v as f32).collect()")
863            }
864        }
865        // Optional(Vec(f32)) needs element-wise cast (NAPI only)
866        TypeRef::Optional(inner)
867            if config.cast_f32_to_f64
868                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
869        {
870            format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as f32).collect())")
871        }
872        // i32→u8/u16/u32/i8/i16 casts (extendr — R maps small ints to i32)
873        TypeRef::Primitive(p) if config.cast_uints_to_i32 && needs_i32_cast(p) => {
874            let core_ty = core_prim_str(p);
875            if optional {
876                format!("{name}: val.{name}.map(|v| v as {core_ty})")
877            } else {
878                format!("{name}: val.{name} as {core_ty}")
879            }
880        }
881        // Optional(i32-needs-cast) with cast_uints_to_i32
882        TypeRef::Optional(inner)
883            if config.cast_uints_to_i32 && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i32_cast(p)) =>
884        {
885            if let TypeRef::Primitive(p) = inner.as_ref() {
886                let core_ty = core_prim_str(p);
887                format!("{name}: val.{name}.map(|v| v as {core_ty})")
888            } else {
889                field_conversion_to_core(name, ty, optional)
890            }
891        }
892        // Vec<u8/u16/u32/i8/i16> needs element-wise i32→core casting
893        TypeRef::Vec(inner)
894            if config.cast_uints_to_i32 && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i32_cast(p)) =>
895        {
896            if let TypeRef::Primitive(p) = inner.as_ref() {
897                let core_ty = core_prim_str(p);
898                if optional {
899                    format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
900                } else {
901                    format!("{name}: val.{name}.into_iter().map(|v| v as {core_ty}).collect()")
902                }
903            } else {
904                field_conversion_to_core(name, ty, optional)
905            }
906        }
907        // f64→u64/usize/isize casts (extendr — R maps large ints to f64)
908        TypeRef::Primitive(p) if config.cast_large_ints_to_f64 && needs_f64_cast(p) => {
909            let core_ty = core_prim_str(p);
910            if optional {
911                format!("{name}: val.{name}.map(|v| v as {core_ty})")
912            } else {
913                format!("{name}: val.{name} as {core_ty}")
914            }
915        }
916        // Optional(f64-needs-cast) with cast_large_ints_to_f64
917        TypeRef::Optional(inner)
918            if config.cast_large_ints_to_f64
919                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
920        {
921            if let TypeRef::Primitive(p) = inner.as_ref() {
922                let core_ty = core_prim_str(p);
923                format!("{name}: val.{name}.map(|v| v as {core_ty})")
924            } else {
925                field_conversion_to_core(name, ty, optional)
926            }
927        }
928        // Vec<u64/usize/isize> needs element-wise f64→core casting
929        TypeRef::Vec(inner)
930            if config.cast_large_ints_to_f64
931                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
932        {
933            if let TypeRef::Primitive(p) = inner.as_ref() {
934                let core_ty = core_prim_str(p);
935                if optional {
936                    format!("{name}: val.{name}.map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
937                } else {
938                    format!("{name}: val.{name}.into_iter().map(|v| v as {core_ty}).collect()")
939                }
940            } else {
941                field_conversion_to_core(name, ty, optional)
942            }
943        }
944        // Map<K, usize/u64/i64/isize/f32> needs value-wise f64→core casting (extendr)
945        TypeRef::Map(_k, v)
946            if config.cast_large_ints_to_f64 && matches!(v.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
947        {
948            if let TypeRef::Primitive(p) = v.as_ref() {
949                let core_ty = core_prim_str(p);
950                if optional {
951                    format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v as {core_ty})).collect())")
952                } else {
953                    format!("{name}: val.{name}.into_iter().map(|(k, v)| (k, v as {core_ty})).collect()")
954                }
955            } else {
956                field_conversion_to_core(name, ty, optional)
957            }
958        }
959        // Skip-type: Named types that can't be auto-converted via Into in the binding→core From
960        // impl (e.g. PHP VisitorHandle which is handled separately by bridge machinery).
961        TypeRef::Named(n) if config.from_binding_skip_types.iter().any(|s| s == n) => {
962            format!("{name}: Default::default()")
963        }
964        TypeRef::Optional(inner) => match inner.as_ref() {
965            TypeRef::Named(n) if config.from_binding_skip_types.iter().any(|s| s == n) => {
966                format!("{name}: Default::default()")
967            }
968            _ => field_conversion_to_core(name, ty, optional),
969        },
970        // Fall through to default for everything else
971        _ => field_conversion_to_core(name, ty, optional),
972    }
973}
974
975/// Apply CoreWrapper transformations to a binding→core conversion expression.
976/// Wraps the value expression with Arc::new(), .into() for Cow, etc.
977pub fn apply_core_wrapper_to_core(
978    conversion: &str,
979    name: &str,
980    core_wrapper: &CoreWrapper,
981    vec_inner_core_wrapper: &CoreWrapper,
982    optional: bool,
983) -> String {
984    // Handle Vec<Arc<T>>: replace .map(Into::into) with .map(|v| std::sync::Arc::new(v.into()))
985    if *vec_inner_core_wrapper == CoreWrapper::Arc {
986        return conversion
987            .replace(
988                ".map(Into::into).collect()",
989                ".map(|v| std::sync::Arc::new(v.into())).collect()",
990            )
991            .replace(
992                "map(|v| v.into_iter().map(Into::into)",
993                "map(|v| v.into_iter().map(|v| std::sync::Arc::new(v.into()))",
994            );
995    }
996
997    match core_wrapper {
998        CoreWrapper::None => conversion.to_string(),
999        CoreWrapper::Cow => {
1000            // Cow<str>: binding String → core Cow via .into()
1001            // The field_conversion already emits "name: val.name" for strings,
1002            // we need to add .into() to convert String → Cow<'static, str>
1003            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
1004                if optional {
1005                    format!("{name}: {expr}.map(Into::into)")
1006                } else if expr == format!("val.{name}") {
1007                    format!("{name}: val.{name}.into()")
1008                } else if expr == "Default::default()" {
1009                    // Sanitized field: Default::default() already resolves to the correct core type
1010                    // (e.g. Cow<'static, str> — adding .into() breaks type inference).
1011                    conversion.to_string()
1012                } else {
1013                    format!("{name}: ({expr}).into()")
1014                }
1015            } else {
1016                conversion.to_string()
1017            }
1018        }
1019        CoreWrapper::Arc => {
1020            // Arc<T>: wrap with Arc::new()
1021            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
1022                if expr == "Default::default()" {
1023                    // Sanitized field: Default::default() resolves to the correct core type;
1024                    // wrapping in Arc::new() would change the type.
1025                    conversion.to_string()
1026                } else if optional {
1027                    format!("{name}: {expr}.map(|v| std::sync::Arc::new(v))")
1028                } else {
1029                    format!("{name}: std::sync::Arc::new({expr})")
1030                }
1031            } else {
1032                conversion.to_string()
1033            }
1034        }
1035        CoreWrapper::Bytes => {
1036            // Bytes: binding Vec<u8> → core bytes::Bytes via .into().
1037            // When TypeRef::Bytes already emitted a conversion (e.g. `val.{name}.into()` or
1038            // `val.{name}.map(Into::into)`), applying another .into() creates an ambiguous
1039            // double-into chain. Detect and dedup: use the already-generated expression as-is
1040            // when it fully covers the conversion, or emit a fresh single .into() for bare fields.
1041            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
1042                let already_converted_non_opt =
1043                    expr == format!("val.{name}.into()") || expr == format!("val.{name}.to_vec().into()");
1044                let already_converted_opt = expr
1045                    .strip_prefix(&format!("val.{name}"))
1046                    .map(|s| s == ".map(Into::into)" || s == ".map(|v| v.to_vec().into())")
1047                    .unwrap_or(false);
1048                if already_converted_non_opt || already_converted_opt {
1049                    // The base conversion already handles Bytes — pass through unchanged.
1050                    conversion.to_string()
1051                } else if optional {
1052                    format!("{name}: {expr}.map(Into::into)")
1053                } else if expr == format!("val.{name}") {
1054                    format!("{name}: val.{name}.into()")
1055                } else if expr == "Default::default()" {
1056                    // Sanitized field: Default::default() already resolves to the correct core type
1057                    // (e.g. bytes::Bytes — adding .into() breaks type inference).
1058                    conversion.to_string()
1059                } else {
1060                    format!("{name}: ({expr}).into()")
1061                }
1062            } else {
1063                conversion.to_string()
1064            }
1065        }
1066        CoreWrapper::ArcMutex => {
1067            // ArcMutex: binding T → core Arc<Mutex<T>> via Arc::new(Mutex::new())
1068            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
1069                if optional {
1070                    format!("{name}: {expr}.map(|v| std::sync::Arc::new(std::sync::Mutex::new(v.into())))")
1071                } else if expr == format!("val.{name}") {
1072                    format!("{name}: std::sync::Arc::new(std::sync::Mutex::new(val.{name}.into()))")
1073                } else {
1074                    format!("{name}: std::sync::Arc::new(std::sync::Mutex::new(({expr}).into()))")
1075                }
1076            } else {
1077                conversion.to_string()
1078            }
1079        }
1080    }
1081}
1082
1083#[cfg(test)]
1084mod tests {
1085    use super::gen_from_binding_to_core;
1086    use alef_core::ir::{CoreWrapper, DefaultValue, FieldDef, TypeDef, TypeRef};
1087
1088    fn type_with_field(field: FieldDef) -> TypeDef {
1089        TypeDef {
1090            name: "ProcessConfig".to_string(),
1091            rust_path: "crate::ProcessConfig".to_string(),
1092            original_rust_path: String::new(),
1093            fields: vec![field],
1094            methods: vec![],
1095            is_opaque: false,
1096            is_clone: true,
1097            is_copy: false,
1098            doc: String::new(),
1099            cfg: None,
1100            is_trait: false,
1101            has_default: true,
1102            has_stripped_cfg_fields: false,
1103            is_return_type: false,
1104            serde_rename_all: None,
1105            has_serde: true,
1106            super_traits: vec![],
1107        }
1108    }
1109
1110    #[test]
1111    fn sanitized_cow_string_field_converts_to_core() {
1112        let field = FieldDef {
1113            name: "language".to_string(),
1114            ty: TypeRef::String,
1115            optional: false,
1116            default: None,
1117            doc: String::new(),
1118            sanitized: true,
1119            is_boxed: false,
1120            type_rust_path: None,
1121            cfg: None,
1122            typed_default: Some(DefaultValue::Empty),
1123            core_wrapper: CoreWrapper::Cow,
1124            vec_inner_core_wrapper: CoreWrapper::None,
1125            newtype_wrapper: None,
1126            serde_rename: None,
1127            serde_flatten: false,
1128        };
1129
1130        let out = gen_from_binding_to_core(&type_with_field(field), "crate");
1131
1132        assert!(out.contains("language: val.language.into()"));
1133        assert!(!out.contains("language: Default::default()"));
1134    }
1135}