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