Skip to main content

alef_codegen/conversions/
core_to_binding.rs

1use ahash::AHashSet;
2use alef_core::ir::{CoreWrapper, PrimitiveType, TypeDef, TypeRef};
3use std::fmt::Write;
4
5use super::ConversionConfig;
6use super::binding_to_core::field_conversion_to_core;
7use super::helpers::is_newtype;
8use super::helpers::{binding_prim_str, core_type_path_remapped, needs_f64_cast, needs_i32_cast, needs_i64_cast};
9
10/// Generate `impl From<core::Type> for BindingType` (core -> binding).
11pub fn gen_from_core_to_binding(typ: &TypeDef, core_import: &str, opaque_types: &AHashSet<String>) -> String {
12    gen_from_core_to_binding_cfg(typ, core_import, opaque_types, &ConversionConfig::default())
13}
14
15/// Generate `impl From<core::Type> for BindingType` with backend-specific config.
16pub fn gen_from_core_to_binding_cfg(
17    typ: &TypeDef,
18    core_import: &str,
19    opaque_types: &AHashSet<String>,
20    config: &ConversionConfig,
21) -> String {
22    let core_path = core_type_path_remapped(typ, core_import, config.source_crate_remaps);
23    let binding_name = format!("{}{}", config.type_name_prefix, typ.name);
24    let mut out = String::with_capacity(256);
25    writeln!(out, "#[allow(clippy::redundant_closure, clippy::useless_conversion)]").ok();
26    writeln!(out, "impl From<{core_path}> for {binding_name} {{").ok();
27    writeln!(out, "    fn from(val: {core_path}) -> Self {{").ok();
28
29    // Newtype structs: extract inner value with val.0
30    if is_newtype(typ) {
31        let field = &typ.fields[0];
32        let inner_expr = match &field.ty {
33            TypeRef::Named(_) => "val.0.into()".to_string(),
34            TypeRef::Path => "val.0.to_string_lossy().to_string()".to_string(),
35            TypeRef::Duration => "val.0.as_millis() as u64".to_string(),
36            _ => "val.0".to_string(),
37        };
38        writeln!(out, "        Self {{ _0: {inner_expr} }}").ok();
39        writeln!(out, "    }}").ok();
40        write!(out, "}}").ok();
41        return out;
42    }
43
44    let optionalized = config.optionalize_defaults && typ.has_default;
45    writeln!(out, "        Self {{").ok();
46    for field in &typ.fields {
47        // Fields referencing excluded types are not present in the binding struct — skip
48        if !config.exclude_types.is_empty()
49            && super::helpers::field_references_excluded_type(&field.ty, config.exclude_types)
50        {
51            continue;
52        }
53        let base_conversion = field_conversion_from_core_cfg(
54            &field.name,
55            &field.ty,
56            field.optional,
57            field.sanitized,
58            opaque_types,
59            config,
60        );
61        // Box<T> fields: dereference before conversion.
62        let base_conversion = if field.is_boxed && matches!(&field.ty, TypeRef::Named(_)) {
63            if field.optional {
64                // Optional<Box<T>>: replace .map(Into::into) with .map(|v| (*v).into())
65                let src = format!("{}: val.{}.map(Into::into)", field.name, field.name);
66                let dst = format!("{}: val.{}.map(|v| (*v).into())", field.name, field.name);
67                if base_conversion == src { dst } else { base_conversion }
68            } else {
69                // Box<T>: replace `val.{name}` with `(*val.{name})`
70                base_conversion.replace(&format!("val.{}", field.name), &format!("(*val.{})", field.name))
71            }
72        } else {
73            base_conversion
74        };
75        // Newtype unwrapping: when the field was resolved from a newtype (e.g. NodeIndex → u32),
76        // unwrap the core newtype by accessing `.0`.
77        // e.g. `source: val.source` → `source: val.source.0`
78        //      `parent: val.parent` → `parent: val.parent.map(|v| v.0)`
79        //      `children: val.children` → `children: val.children.iter().map(|v| v.0).collect()`
80        let base_conversion = if field.newtype_wrapper.is_some() {
81            match &field.ty {
82                TypeRef::Optional(_) => {
83                    // Replace `val.{name}` with `val.{name}.map(|v| v.0)` in the generated expression
84                    base_conversion.replace(
85                        &format!("val.{}", field.name),
86                        &format!("val.{}.map(|v| v.0)", field.name),
87                    )
88                }
89                TypeRef::Vec(_) => {
90                    // Replace `val.{name}` with `val.{name}.iter().map(|v| v.0).collect()` in expression
91                    base_conversion.replace(
92                        &format!("val.{}", field.name),
93                        &format!("val.{}.iter().map(|v| v.0).collect::<Vec<_>>()", field.name),
94                    )
95                }
96                // When `optional=true` and `ty` is a plain Primitive (not TypeRef::Optional), the core
97                // field is actually `Option<NewtypeT>`, so we must use `.map(|v| v.0)` not `.0`.
98                _ if field.optional => base_conversion.replace(
99                    &format!("val.{}", field.name),
100                    &format!("val.{}.map(|v| v.0)", field.name),
101                ),
102                _ => {
103                    // Direct field: append `.0` to access the inner primitive
104                    base_conversion.replace(&format!("val.{}", field.name), &format!("val.{}.0", field.name))
105                }
106            }
107        } else {
108            base_conversion
109        };
110        // When field.optional=true AND field.ty=Optional(T), the binding struct flattens
111        // Option<Option<T>> to Option<T>. Core produces Option<Option<T>>, binding needs
112        // Option<T>. Generate the conversion by treating the pre-flattened field as Option<T>:
113        // call the standard conversion for the inner type T with optional=true, substituting
114        // val.{name}.flatten() for val.{name} so all cast/conversion logic applies to T.
115        let is_flattened_optional = field.optional && matches!(field.ty, TypeRef::Optional(_));
116        let base_conversion = if is_flattened_optional {
117            if let TypeRef::Optional(inner) = &field.ty {
118                // Produce the conversion as if the field is Option<inner> with value val.name.flatten()
119                let inner_conv = field_conversion_from_core_cfg(
120                    &field.name,
121                    inner.as_ref(),
122                    true,
123                    field.sanitized,
124                    opaque_types,
125                    config,
126                );
127                // inner_conv references val.{name}; replace with val.{name}.flatten()
128                inner_conv.replace(&format!("val.{}", field.name), &format!("val.{}.flatten()", field.name))
129            } else {
130                base_conversion
131            }
132        } else {
133            base_conversion
134        };
135        // Optionalized non-optional fields need Some() wrapping in core→binding direction.
136        // This covers both NAPI-style full optionalization and PyO3-style Duration optionalization.
137        // Flattened-optional fields are already handled above with the correct type.
138        let needs_some_wrap = !is_flattened_optional
139            && ((optionalized && !field.optional)
140                || (config.option_duration_on_defaults
141                    && typ.has_default
142                    && !field.optional
143                    && matches!(field.ty, TypeRef::Duration)));
144        let conversion = if needs_some_wrap {
145            // Extract the value expression after "name: " and wrap in Some()
146            if let Some(expr) = base_conversion.strip_prefix(&format!("{}: ", field.name)) {
147                format!("{}: Some({})", field.name, expr)
148            } else {
149                base_conversion
150            }
151        } else {
152            base_conversion
153        };
154        // Opaque Named fields without CoreWrapper::Arc (e.g. visitor: Object<'static>) cannot be
155        // auto-converted via Arc::new — the binding stores a raw host object that needs a bridge.
156        // Emit Default::default() and let the caller (e.g. the convert function) set it separately.
157        let is_opaque_no_wrapper_field = field.core_wrapper == CoreWrapper::None
158            && matches!(&field.ty, TypeRef::Named(n) if config
159                .opaque_types
160                .is_some_and(|opaque| opaque.contains(n.as_str())));
161        // CoreWrapper: unwrap Arc, convert Cow→String, Bytes→Vec<u8>
162        // For sanitized fields, still apply Cow→String conversion: Cow<'_, str> sanitizes to
163        // TypeRef::String and the Debug-formatted fallback produces quotes, but Cow implements
164        // Display so .to_string() (emitted by apply_core_wrapper_from_core for Cow) is correct.
165        // Other sanitized fields (unknown Named types) still fall through to Debug formatting.
166        let conversion = if is_opaque_no_wrapper_field {
167            format!("{}: Default::default()", field.name)
168        } else if !field.sanitized || field.core_wrapper == alef_core::ir::CoreWrapper::Cow {
169            apply_core_wrapper_from_core(
170                &conversion,
171                &field.name,
172                &field.core_wrapper,
173                &field.vec_inner_core_wrapper,
174                field.optional,
175            )
176        } else {
177            conversion
178        };
179        // Skip cfg-gated fields — they don't exist in the binding struct
180        if field.cfg.is_some() {
181            continue;
182        }
183        // In core→binding direction, the binding struct field may be keyword-escaped
184        // (e.g. `class_` for `class`). The generated conversion has `field.name: expr`
185        // on the left side — rename it to `binding_name: expr` when needed.
186        let binding_field = config.binding_field_name_owned(&typ.name, &field.name);
187        let conversion = if binding_field != field.name {
188            if let Some(expr) = conversion.strip_prefix(&format!("{}: ", field.name)) {
189                format!("{binding_field}: {expr}")
190            } else {
191                conversion
192            }
193        } else {
194            conversion
195        };
196        writeln!(out, "            {conversion},").ok();
197    }
198
199    writeln!(out, "        }}").ok();
200    writeln!(out, "    }}").ok();
201    write!(out, "}}").ok();
202    out
203}
204
205/// Same but for core -> binding direction.
206/// Some types are asymmetric (PathBuf→String, sanitized fields need .to_string()).
207pub fn field_conversion_from_core(
208    name: &str,
209    ty: &TypeRef,
210    optional: bool,
211    sanitized: bool,
212    opaque_types: &AHashSet<String>,
213) -> String {
214    // Sanitized fields: the binding type differs from core (e.g. Box<str>→String, Cow<str>→String).
215    // Box<str>, Cow<str>, and Arc<str> all implement Display, so use .to_string() not {:?}.
216    // {:?} on string-like types produces debug-escaped output with surrounding quotes.
217    if sanitized {
218        // Vec<Primitive>: sanitized from tuple types like (u32, u32) → Vec<u32>.
219        // Core has a tuple, binding expects Vec — destructure the tuple.
220        if let TypeRef::Vec(inner) = ty {
221            if matches!(inner.as_ref(), TypeRef::Primitive(_)) {
222                if optional {
223                    return format!(
224                        "{name}: val.{name}.map(|t| {{ let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect(); arr }})"
225                    );
226                }
227                return format!("{name}: vec![val.{name}.0 as _, val.{name}.1 as _]");
228            }
229        }
230        // Optional(Vec<Primitive>): sanitized from Option<(T, T)> → Option<Vec<T>>.
231        if let TypeRef::Optional(opt_inner) = ty {
232            if let TypeRef::Vec(vec_inner) = opt_inner.as_ref() {
233                if matches!(vec_inner.as_ref(), TypeRef::Primitive(_)) {
234                    return format!("{name}: val.{name}.map(|t| vec![t.0 as _, t.1 as _])");
235                }
236            }
237        }
238        // Map(String, String): sanitized from Map(Box<str>, Box<str>) etc.
239        if let TypeRef::Map(k, v) = ty {
240            if matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String) {
241                if optional {
242                    return format!(
243                        "{name}: val.{name}.as_ref().map(|m| m.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect())"
244                    );
245                }
246                return format!(
247                    "{name}: val.{name}.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()"
248                );
249            }
250        }
251        // Vec<String>: sanitized from Vec<Box<str>>, Vec<Cow<str>>, Vec<Named>, etc.
252        // Use Debug formatting — the original core type may not implement Display.
253        if let TypeRef::Vec(inner) = ty {
254            if matches!(inner.as_ref(), TypeRef::String) {
255                if optional {
256                    return format!(
257                        "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| format!(\"{{:?}}\", i)).collect())"
258                    );
259                }
260                return format!("{name}: val.{name}.iter().map(|i| format!(\"{{:?}}\", i)).collect()");
261            }
262        }
263        // Optional<Vec<String>>: sanitized from Optional<Vec<Box<str>>>, Optional<Vec<Cow<str>>>, etc.
264        if let TypeRef::Optional(opt_inner) = ty {
265            if let TypeRef::Vec(vec_inner) = opt_inner.as_ref() {
266                if matches!(vec_inner.as_ref(), TypeRef::String) {
267                    return format!(
268                        "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| format!(\"{{:?}}\", i)).collect())"
269                    );
270                }
271            }
272        }
273        // String: sanitized from Box<str>, Cow<str>, (u32, u32), etc.
274        // Use Debug formatting — it works for all types (including tuples) and avoids Display
275        // trait bound failures when the original core type doesn't implement Display.
276        // Note: Cow<str> is handled before this point via the CoreWrapper::Cow path above.
277        if matches!(ty, TypeRef::String) {
278            if optional {
279                return format!("{name}: val.{name}.as_ref().map(|v| format!(\"{{v:?}}\"))");
280            }
281            return format!("{name}: format!(\"{{:?}}\", val.{name})");
282        }
283        // Fallback for truly unknown sanitized types — the core type may not implement Display,
284        // so use Debug formatting which is always available (required by the sanitized field's derive).
285        if optional {
286            return format!("{name}: val.{name}.as_ref().map(|v| format!(\"{{v:?}}\"))");
287        }
288        return format!("{name}: format!(\"{{:?}}\", val.{name})");
289    }
290    match ty {
291        // Duration: core uses std::time::Duration, binding uses u64 (millis)
292        TypeRef::Duration => {
293            if optional {
294                return format!("{name}: val.{name}.map(|d| d.as_millis() as u64)");
295            }
296            format!("{name}: val.{name}.as_millis() as u64")
297        }
298        // Path: core uses PathBuf, binding uses String — PathBuf→String needs special handling
299        TypeRef::Path => {
300            if optional {
301                format!("{name}: val.{name}.map(|p| p.to_string_lossy().to_string())")
302            } else {
303                format!("{name}: val.{name}.to_string_lossy().to_string()")
304            }
305        }
306        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Path) => {
307            format!("{name}: val.{name}.map(|p| p.to_string_lossy().to_string())")
308        }
309        // Char: core uses char, binding uses String — convert char to string
310        TypeRef::Char => {
311            if optional {
312                format!("{name}: val.{name}.map(|c| c.to_string())")
313            } else {
314                format!("{name}: val.{name}.to_string()")
315            }
316        }
317        // Bytes: core uses bytes::Bytes, binding uses Vec<u8> or napi `Buffer`.
318        // `.into()` is a no-op when destination is Vec<u8> (identity From) and
319        // a Vec→Buffer wrap when destination is `napi::bindgen_prelude::Buffer`.
320        TypeRef::Bytes => {
321            if optional {
322                format!("{name}: val.{name}.map(|v| v.to_vec().into())")
323            } else {
324                format!("{name}: val.{name}.to_vec().into()")
325            }
326        }
327        // Opaque Named types: wrap in Arc to create the binding wrapper
328        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
329            if optional {
330                format!("{name}: val.{name}.map(|v| {n} {{ inner: Arc::new(v) }})")
331            } else {
332                format!("{name}: {n} {{ inner: Arc::new(val.{name}) }}")
333            }
334        }
335        // Json: core uses serde_json::Value, binding uses String — use .to_string()
336        TypeRef::Json => {
337            if optional {
338                format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
339            } else {
340                format!("{name}: val.{name}.to_string()")
341            }
342        }
343        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Json) => {
344            format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
345        }
346        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Json) => {
347            if optional {
348                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| i.to_string()).collect())")
349            } else {
350                format!("{name}: val.{name}.iter().map(ToString::to_string).collect()")
351            }
352        }
353        // Vec<Optional<Json>>: each element is Option<Value> → Option<String>
354        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Optional(oi) if matches!(oi.as_ref(), TypeRef::Json)) => {
355            if optional {
356                format!(
357                    "{name}: val.{name}.as_ref().map(|v| v.iter().map(|i| i.as_ref().map(ToString::to_string)).collect())"
358                )
359            } else {
360                format!("{name}: val.{name}.iter().map(|i| i.as_ref().map(ToString::to_string)).collect()")
361            }
362        }
363        // Map with Json values: core uses HashMap<K, serde_json::Value>, binding uses HashMap<K, String>.
364        // Always emit `k.to_string()` so Cow<'_, str> / Box<str> / Arc<str> keys (which the type
365        // resolver normalizes to TypeRef::String) convert correctly. For an actual `String` key
366        // this is a clone, accepted under the existing `#[allow(clippy::useless_conversion)]`.
367        TypeRef::Map(_k, v) if matches!(v.as_ref(), TypeRef::Json) => {
368            if optional {
369                format!(
370                    "{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect())"
371                )
372            } else {
373                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()")
374            }
375        }
376        // Map with Json keys: core uses HashMap<serde_json::Value, V>, binding uses HashMap<String, V>
377        TypeRef::Map(k, _v) if matches!(k.as_ref(), TypeRef::Json) => {
378            if optional {
379                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.to_string(), v)).collect())")
380            } else {
381                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.to_string(), v)).collect()")
382            }
383        }
384        // Map<String, String>: core may have Box<str> keys/values, binding has String keys/values.
385        // Emit .map() with .into() conversions, which are no-ops when both sides are String.
386        // This handles cases like HashMap<Box<str>, Box<str>> (core) → HashMap<String, String> (binding).
387        TypeRef::Map(k, v) if matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String) => {
388            if optional {
389                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect())")
390            } else {
391                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k.into(), v.into())).collect()")
392            }
393        }
394        // Map<K, Named>: each value needs .into() to convert core→binding
395        TypeRef::Map(_k, v) if matches!(v.as_ref(), TypeRef::Named(_)) => {
396            if optional {
397                format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())")
398            } else {
399                format!("{name}: val.{name}.into_iter().map(|(k, v)| (k, v.into())).collect()")
400            }
401        }
402        // Optional(Map<K, Named>): same but wrapped in Option
403        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Map(_k, v) if matches!(v.as_ref(), TypeRef::Named(_))) =>
404        {
405            format!("{name}: val.{name}.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())")
406        }
407        // Vec<Named>: each element needs .into() to convert core→binding
408        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => {
409            if optional {
410                format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
411            } else {
412                format!("{name}: val.{name}.into_iter().map(Into::into).collect()")
413            }
414        }
415        // Optional(Vec<Named>): same but wrapped in Option
416        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_))) =>
417        {
418            format!("{name}: val.{name}.map(|v| v.into_iter().map(Into::into).collect())")
419        }
420        // Everything else is symmetric
421        _ => field_conversion_to_core(name, ty, optional),
422    }
423}
424
425/// Core→binding field conversion with backend-specific config.
426pub fn field_conversion_from_core_cfg(
427    name: &str,
428    ty: &TypeRef,
429    optional: bool,
430    sanitized: bool,
431    opaque_types: &AHashSet<String>,
432    config: &ConversionConfig,
433) -> String {
434    // Sanitized fields: for WASM (map_uses_jsvalue), Map and Vec<Json> fields target JsValue
435    // and need serde_wasm_bindgen::to_value() instead of iterator-based .collect().
436    // Note: Vec<String> sanitized does NOT use the JsValue path because Vec<String> maps to
437    // Vec<String> in WASM (not JsValue) — use the normal sanitized iterator path instead.
438    if sanitized {
439        if config.map_uses_jsvalue {
440            // Map(String, String) sanitized → JsValue (HashMap maps to JsValue in WASM)
441            // Use js_sys::JSON::parse(json_str) to get a plain JS object (not ES6 Map).
442            if let TypeRef::Map(k, v) = ty {
443                if matches!(k.as_ref(), TypeRef::String) && matches!(v.as_ref(), TypeRef::String) {
444                    if optional {
445                        return format!(
446                            "{name}: val.{name}.as_ref().and_then(|v| serde_json::to_string(v).ok()).and_then(|s| js_sys::JSON::parse(&s).ok())"
447                        );
448                    }
449                    return format!(
450                        "{name}: js_sys::JSON::parse(&serde_json::to_string(&val.{name}).unwrap_or_default()).unwrap_or(JsValue::NULL)"
451                    );
452                }
453            }
454            // Vec<Json> sanitized → JsValue (Vec<Json> maps to JsValue in WASM via nested-vec path)
455            if let TypeRef::Vec(inner) = ty {
456                if matches!(inner.as_ref(), TypeRef::Json) {
457                    if optional {
458                        return format!(
459                            "{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())"
460                        );
461                    }
462                    return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
463                }
464            }
465        }
466        return field_conversion_from_core(name, ty, optional, sanitized, opaque_types);
467    }
468
469    // Vec<Named>→String core→binding: binding holds JSON string, core has Vec<Named>.
470    // Only apply serde round-trip for Vec<Named> types (complex structs that can't cross FFI).
471    // Vec<String>, Vec<Primitive>, etc. stay as-is since they map directly.
472    if config.vec_named_to_string {
473        if let TypeRef::Vec(inner) = ty {
474            if matches!(inner.as_ref(), TypeRef::Named(_)) {
475                if optional {
476                    return format!("{name}: val.{name}.as_ref().and_then(|v| serde_json::to_string(v).ok())");
477                }
478                return format!("{name}: serde_json::to_string(&val.{name}).unwrap_or_default()");
479            }
480        }
481    }
482
483    // Map→String core→binding: binding holds Debug-formatted string, core has HashMap.
484    // Used by Rustler (Elixir NIFs) where HashMap cannot cross the NIF boundary directly.
485    if config.map_as_string && matches!(ty, TypeRef::Map(_, _)) {
486        if optional {
487            return format!("{name}: val.{name}.as_ref().map(|m| format!(\"{{m:?}}\"))");
488        }
489        return format!("{name}: format!(\"{{:?}}\", val.{name})");
490    }
491    if config.map_as_string {
492        if let TypeRef::Optional(inner) = ty {
493            if matches!(inner.as_ref(), TypeRef::Map(_, _)) {
494                return format!("{name}: val.{name}.as_ref().map(|m| format!(\"{{m:?}}\"))");
495            }
496        }
497    }
498
499    // WASM JsValue: use serde_wasm_bindgen for nested Vec, js_sys::JSON::parse for Map types.
500    // Maps must go through JSON string → js_sys::JSON::parse to get a plain JS object.
501    // serde_wasm_bindgen::to_value always creates ES6 Maps for serialize_map calls.
502    if config.map_uses_jsvalue {
503        let is_nested_vec = matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Vec(_)));
504        let is_map = matches!(ty, TypeRef::Map(_, _));
505        if is_nested_vec {
506            if optional {
507                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())");
508            }
509            return format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)");
510        }
511        if is_map {
512            if optional {
513                return format!(
514                    "{name}: val.{name}.as_ref().and_then(|v| serde_json::to_string(v).ok()).and_then(|s| js_sys::JSON::parse(&s).ok())"
515                );
516            }
517            return format!(
518                "{name}: js_sys::JSON::parse(&serde_json::to_string(&val.{name}).unwrap_or_default()).unwrap_or(JsValue::NULL)"
519            );
520        }
521        if let TypeRef::Optional(inner) = ty {
522            let is_inner_nested = matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Vec(_)));
523            let is_inner_map = matches!(inner.as_ref(), TypeRef::Map(_, _));
524            if is_inner_nested {
525                return format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())");
526            }
527            if is_inner_map {
528                return format!(
529                    "{name}: val.{name}.as_ref().and_then(|v| serde_json::to_string(v).ok()).and_then(|s| js_sys::JSON::parse(&s).ok())"
530                );
531            }
532        }
533    }
534
535    let prefix = config.type_name_prefix;
536    let is_enum_string = |n: &str| -> bool { config.enum_string_names.as_ref().is_some_and(|names| names.contains(n)) };
537
538    match ty {
539        // i64 casting for large int primitives
540        TypeRef::Primitive(p) if config.cast_large_ints_to_i64 && needs_i64_cast(p) => {
541            let cast_to = binding_prim_str(p);
542            if optional {
543                format!("{name}: val.{name}.map(|v| v as {cast_to})")
544            } else {
545                format!("{name}: val.{name} as {cast_to}")
546            }
547        }
548        // Optional(large_int) with i64 casting
549        TypeRef::Optional(inner)
550            if config.cast_large_ints_to_i64
551                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
552        {
553            if let TypeRef::Primitive(p) = inner.as_ref() {
554                let cast_to = binding_prim_str(p);
555                format!("{name}: val.{name}.map(|v| v as {cast_to})")
556            } else {
557                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
558            }
559        }
560        // i32 casting for small uint primitives (extendr/R only)
561        TypeRef::Primitive(p) if config.cast_uints_to_i32 && needs_i32_cast(p) => {
562            if optional {
563                format!("{name}: val.{name}.map(|v| v as i32)")
564            } else {
565                format!("{name}: val.{name} as i32")
566            }
567        }
568        // Optional(small_uint) with i32 casting
569        TypeRef::Optional(inner)
570            if config.cast_uints_to_i32 && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i32_cast(p)) =>
571        {
572            format!("{name}: val.{name}.map(|v| v as i32)")
573        }
574        // Vec<u8/u16/u32/i8/i16> needs element-wise core→i32 casting (extendr/R only)
575        TypeRef::Vec(inner)
576            if config.cast_uints_to_i32 && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i32_cast(p)) =>
577        {
578            if let TypeRef::Primitive(_p) = inner.as_ref() {
579                if optional {
580                    format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as i32).collect())")
581                } else {
582                    format!("{name}: val.{name}.iter().map(|&v| v as i32).collect()")
583                }
584            } else {
585                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
586            }
587        }
588        // f64 casting for large int primitives (extendr/R only)
589        TypeRef::Primitive(p) if config.cast_large_ints_to_f64 && needs_f64_cast(p) => {
590            if optional {
591                format!("{name}: val.{name}.map(|v| v as f64)")
592            } else {
593                format!("{name}: val.{name} as f64")
594            }
595        }
596        // Optional(large_int) with f64 casting
597        TypeRef::Optional(inner)
598            if config.cast_large_ints_to_f64
599                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
600        {
601            format!("{name}: val.{name}.map(|v| v as f64)")
602        }
603        // Vec<usize/u64/i64/isize/f32> needs element-wise f64 cast for extendr/R backend
604        TypeRef::Vec(inner)
605            if config.cast_large_ints_to_f64
606                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
607        {
608            if optional {
609                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
610            } else {
611                format!("{name}: val.{name}.iter().map(|&v| v as f64).collect()")
612            }
613        }
614        // Optional(Vec(usize/u64/i64/isize/f32)) needs element-wise f64 cast
615        TypeRef::Optional(inner)
616            if config.cast_large_ints_to_f64
617                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p))) =>
618        {
619            format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
620        }
621        // Vec<Vec<usize/u64/i64/isize/f32>> needs nested element-wise f64 cast (embeddings)
622        TypeRef::Vec(outer)
623            if config.cast_large_ints_to_f64
624                && matches!(outer.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p))) =>
625        {
626            if optional {
627                format!(
628                    "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect())"
629                )
630            } else {
631                format!("{name}: val.{name}.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect()")
632            }
633        }
634        // Optional(Vec<Vec<usize/u64/i64/isize/f32>>) needs nested element-wise f64 cast
635        TypeRef::Optional(inner)
636            if config.cast_large_ints_to_f64
637                && matches!(inner.as_ref(), TypeRef::Vec(outer) if matches!(outer.as_ref(), TypeRef::Vec(prim) if matches!(prim.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)))) =>
638        {
639            format!(
640                "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect())"
641            )
642        }
643        // Map values that are usize/u64/i64/isize/f32 stored as f64 in binding → cast when reading core
644        TypeRef::Map(_k, v)
645            if config.cast_large_ints_to_f64 && matches!(v.as_ref(), TypeRef::Primitive(p) if needs_f64_cast(p)) =>
646        {
647            if optional {
648                format!("{name}: val.{name}.as_ref().map(|m| m.iter().map(|(k, v)| (k.clone(), *v as f64)).collect())")
649            } else {
650                format!("{name}: val.{name}.iter().map(|(k, v)| (k.clone(), *v as f64)).collect()")
651            }
652        }
653        // Duration with f64 casting (R: no u64, use f64 millis)
654        TypeRef::Duration if config.cast_large_ints_to_f64 => {
655            if optional {
656                format!("{name}: val.{name}.map(|d| d.as_millis() as f64)")
657            } else {
658                format!("{name}: val.{name}.as_millis() as f64")
659            }
660        }
661        // f32→f64 casting (NAPI only)
662        TypeRef::Primitive(PrimitiveType::F32) if config.cast_f32_to_f64 => {
663            if optional {
664                format!("{name}: val.{name}.map(|v| v as f64)")
665            } else {
666                format!("{name}: val.{name} as f64")
667            }
668        }
669        // Duration with i64 casting
670        TypeRef::Duration if config.cast_large_ints_to_i64 => {
671            if optional {
672                format!("{name}: val.{name}.map(|d| d.as_millis() as u64 as i64)")
673            } else {
674                format!("{name}: val.{name}.as_millis() as u64 as i64")
675            }
676        }
677        // Opaque Named types with prefix: wrap in Arc with prefixed binding name
678        TypeRef::Named(n) if opaque_types.contains(n.as_str()) && !prefix.is_empty() => {
679            let prefixed = format!("{prefix}{n}");
680            if optional {
681                format!("{name}: val.{name}.map(|v| {prefixed} {{ inner: Arc::new(v) }})")
682            } else {
683                format!("{name}: {prefixed} {{ inner: Arc::new(val.{name}) }}")
684            }
685        }
686        // Enum-to-String Named types (PHP pattern)
687        TypeRef::Named(n) if is_enum_string(n) => {
688            // Use serde serialization to get the correct serde(rename) value, not Debug format.
689            // serde_json::to_value gives Value::String("auto") which we extract.
690            if optional {
691                format!(
692                    "{name}: val.{name}.as_ref().map(|v| serde_json::to_value(v).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default())"
693                )
694            } else {
695                format!(
696                    "{name}: serde_json::to_value(val.{name}).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default()"
697                )
698            }
699        }
700        // Vec<Enum-to-String> Named types: element-wise serde serialization
701        TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(n) if is_enum_string(n)) => {
702            if optional {
703                format!(
704                    "{name}: val.{name}.as_ref().map(|v| v.iter().map(|x| serde_json::to_value(x).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default()).collect())"
705                )
706            } else {
707                format!(
708                    "{name}: val.{name}.iter().map(|v| serde_json::to_value(v).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default()).collect()"
709                )
710            }
711        }
712        // Optional(Vec<Enum-to-String>) Named types (PHP pattern)
713        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(n) if is_enum_string(n))) =>
714        {
715            format!(
716                "{name}: val.{name}.as_ref().map(|v| v.iter().map(|x| serde_json::to_value(x).ok().and_then(|s| s.as_str().map(String::from)).unwrap_or_default()).collect())"
717            )
718        }
719        // Vec<f32> needs element-wise cast to f64 when f32→f64 mapping is active
720        TypeRef::Vec(inner)
721            if config.cast_f32_to_f64 && matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32)) =>
722        {
723            if optional {
724                format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
725            } else {
726                format!("{name}: val.{name}.iter().map(|&v| v as f64).collect()")
727            }
728        }
729        // Optional(Vec(f32)) needs element-wise cast to f64
730        TypeRef::Optional(inner)
731            if config.cast_f32_to_f64
732                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
733        {
734            format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as f64).collect())")
735        }
736        // Optional(Vec(u64/usize/isize)) needs element-wise i64 casting
737        TypeRef::Optional(inner)
738            if config.cast_large_ints_to_i64
739                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p))) =>
740        {
741            if let TypeRef::Vec(vi) = inner.as_ref() {
742                if let TypeRef::Primitive(p) = vi.as_ref() {
743                    let cast_to = binding_prim_str(p);
744                    if sanitized {
745                        // Sanitized from Option<(T, T)> → Option<Vec<T>>: destructure tuple
746                        format!("{name}: val.{name}.map(|(a, b)| vec![a as {cast_to}, b as {cast_to}])")
747                    } else {
748                        format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as {cast_to}).collect())")
749                    }
750                } else {
751                    field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
752                }
753            } else {
754                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
755            }
756        }
757        // Vec<Vec<f32>> needs nested element-wise cast to f64 (for embeddings, etc.)
758        TypeRef::Vec(outer)
759            if config.cast_f32_to_f64
760                && matches!(outer.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::F32))) =>
761        {
762            if optional {
763                format!(
764                    "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect())"
765                )
766            } else {
767                format!("{name}: val.{name}.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect()")
768            }
769        }
770        // Optional(Vec<Vec<f32>>) needs nested element-wise cast to f64
771        TypeRef::Optional(inner)
772            if config.cast_f32_to_f64
773                && matches!(inner.as_ref(), TypeRef::Vec(outer) if matches!(outer.as_ref(), TypeRef::Vec(prim) if matches!(prim.as_ref(), TypeRef::Primitive(PrimitiveType::F32)))) =>
774        {
775            format!(
776                "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as f64).collect()).collect())"
777            )
778        }
779        // Optional with i64-cast inner
780        TypeRef::Optional(inner)
781            if config.cast_large_ints_to_i64
782                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
783        {
784            if let TypeRef::Primitive(p) = inner.as_ref() {
785                let cast_to = binding_prim_str(p);
786                format!("{name}: val.{name}.map(|v| v as {cast_to})")
787            } else {
788                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
789            }
790        }
791        // HashMap value type casting: when value type needs i64 casting
792        TypeRef::Map(_k, v)
793            if config.cast_large_ints_to_i64 && matches!(v.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
794        {
795            if let TypeRef::Primitive(p) = v.as_ref() {
796                let cast_to = binding_prim_str(p);
797                if optional {
798                    format!(
799                        "{name}: val.{name}.as_ref().map(|m| m.iter().map(|(k, v)| (k.clone(), *v as {cast_to})).collect())"
800                    )
801                } else {
802                    format!("{name}: val.{name}.iter().map(|(k, v)| (k.clone(), *v as {cast_to})).collect()")
803                }
804            } else {
805                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
806            }
807        }
808        // Vec<u64/usize/isize> needs element-wise i64 casting (core→binding)
809        TypeRef::Vec(inner)
810            if config.cast_large_ints_to_i64
811                && matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p)) =>
812        {
813            if let TypeRef::Primitive(p) = inner.as_ref() {
814                let cast_to = binding_prim_str(p);
815                if sanitized {
816                    // Sanitized from tuple (T, T) → Vec<T>: destructure tuple into vec
817                    if optional {
818                        format!("{name}: val.{name}.map(|(a, b)| vec![a as {cast_to}, b as {cast_to}])")
819                    } else {
820                        format!("{name}: {{ let (a, b) = val.{name}; vec![a as {cast_to}, b as {cast_to}] }}")
821                    }
822                } else if optional {
823                    format!("{name}: val.{name}.as_ref().map(|v| v.iter().map(|&x| x as {cast_to}).collect())")
824                } else {
825                    format!("{name}: val.{name}.iter().map(|&v| v as {cast_to}).collect()")
826                }
827            } else {
828                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
829            }
830        }
831        // Vec<Vec<u64/usize/isize>> needs nested element-wise i64 casting (core→binding)
832        TypeRef::Vec(outer)
833            if config.cast_large_ints_to_i64
834                && matches!(outer.as_ref(), TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(p) if needs_i64_cast(p))) =>
835        {
836            if let TypeRef::Vec(inner) = outer.as_ref() {
837                if let TypeRef::Primitive(p) = inner.as_ref() {
838                    let cast_to = binding_prim_str(p);
839                    if optional {
840                        format!(
841                            "{name}: val.{name}.as_ref().map(|v| v.iter().map(|inner| inner.iter().map(|&x| x as {cast_to}).collect()).collect())"
842                        )
843                    } else {
844                        format!(
845                            "{name}: val.{name}.iter().map(|inner| inner.iter().map(|&x| x as {cast_to}).collect()).collect()"
846                        )
847                    }
848                } else {
849                    field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
850                }
851            } else {
852                field_conversion_from_core(name, ty, optional, sanitized, opaque_types)
853            }
854        }
855        // Json→String: core uses serde_json::Value, binding uses String (PHP)
856        TypeRef::Json if config.json_to_string => {
857            if optional {
858                format!("{name}: val.{name}.as_ref().map(ToString::to_string)")
859            } else {
860                format!("{name}: val.{name}.to_string()")
861            }
862        }
863        // Json→JsValue: core uses serde_json::Value, binding uses JsValue (WASM)
864        TypeRef::Json if config.map_uses_jsvalue => {
865            if optional {
866                format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
867            } else {
868                format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)")
869            }
870        }
871        // Vec<Json>→JsValue: core uses Vec<serde_json::Value>, binding uses JsValue (WASM)
872        TypeRef::Vec(inner) if config.map_uses_jsvalue && matches!(inner.as_ref(), TypeRef::Json) => {
873            if optional {
874                format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
875            } else {
876                format!("{name}: serde_wasm_bindgen::to_value(&val.{name}).unwrap_or(JsValue::NULL)")
877            }
878        }
879        // Optional(Vec<Json>)→JsValue (WASM)
880        TypeRef::Optional(inner)
881            if config.map_uses_jsvalue
882                && matches!(inner.as_ref(), TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Json)) =>
883        {
884            format!("{name}: val.{name}.as_ref().and_then(|v| serde_wasm_bindgen::to_value(v).ok())")
885        }
886        // Fall through to default (handles paths, opaque without prefix, etc.)
887        _ => field_conversion_from_core(name, ty, optional, sanitized, opaque_types),
888    }
889}
890
891/// Apply CoreWrapper transformations for core→binding direction.
892/// Unwraps Arc, converts Cow→String, Bytes→Vec<u8>.
893fn apply_core_wrapper_from_core(
894    conversion: &str,
895    name: &str,
896    core_wrapper: &CoreWrapper,
897    vec_inner_core_wrapper: &CoreWrapper,
898    optional: bool,
899) -> String {
900    // Handle Vec<Arc<T>>: unwrap Arc elements
901    if *vec_inner_core_wrapper == CoreWrapper::Arc {
902        return conversion
903            .replace(".map(Into::into).collect()", ".map(|v| (*v).clone().into()).collect()")
904            .replace(
905                "map(|v| v.into_iter().map(Into::into)",
906                "map(|v| v.into_iter().map(|v| (*v).clone().into())",
907            );
908    }
909
910    match core_wrapper {
911        CoreWrapper::None => conversion.to_string(),
912        CoreWrapper::Cow => {
913            // Cow<str> → String: core val.name is Cow<'static, str>, binding needs String.
914            // Always emit val.{name}.into_owned() regardless of what the base conversion emits.
915            // This handles both the normal path (base = "name: val.name") and the sanitized path
916            // (base = "name: format!(\"{:?}\", val.name)") which produces debug-escaped strings.
917            // When the binding has been optionalized (e.g. NAPI default-optional fields), the
918            // upstream pass already wrapped the conversion in Some(...) — preserve that wrap.
919            let prefix = format!("{name}: ");
920            let already_some_wrapped = conversion
921                .strip_prefix(&prefix)
922                .is_some_and(|expr| expr.starts_with("Some("));
923            if optional {
924                format!("{name}: val.{name}.as_ref().map(|v| v.to_string())")
925            } else if already_some_wrapped {
926                format!("{name}: Some(val.{name}.to_string())")
927            } else {
928                format!("{name}: val.{name}.to_string()")
929            }
930        }
931        CoreWrapper::Arc => {
932            // Arc<T> → T: unwrap via clone.
933            //
934            // Special case: opaque Named types build the binding wrapper with
935            // `{ inner: Arc::new(v) }` in the base conversion, but when the core
936            // field is `Arc<T>`, `v` IS already the `Arc<T>` — wrapping it again
937            // with `Arc::new` produces `Arc<Arc<T>>`.  Detect this pattern and
938            // replace `Arc::new(v)` with `v`, and `Arc::new(val.{name})` with
939            // `val.{name}`, then return without adding an extra unwrap chain.
940            if conversion.contains("{ inner: Arc::new(") {
941                return conversion.replace("{ inner: Arc::new(v) }", "{ inner: v }").replace(
942                    &format!("{{ inner: Arc::new(val.{name}) }}"),
943                    &format!("{{ inner: val.{name} }}"),
944                );
945            }
946            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
947                if optional {
948                    // When the base conversion is the simple passthrough `val.{name}`,
949                    // the Option carries Arc<T> elements; deref-clone each.
950                    // When the base is already a complex expression (e.g.
951                    // `val.{name}.as_ref().map(ToString::to_string)` for Json fields),
952                    // the Arc is transparently handled via Display/Deref coercion;
953                    // chaining another `.map(|v| (*v).clone().into())` would operate
954                    // on the already-converted value (e.g. String) and emit invalid
955                    // codegen such as `(*String).clone()` (since str: !Clone).
956                    let simple_passthrough = format!("val.{name}");
957                    if expr == simple_passthrough {
958                        format!("{name}: {expr}.map(|v| (*v).clone().into())")
959                    } else {
960                        format!("{name}: {expr}")
961                    }
962                } else {
963                    let unwrapped = expr.replace(&format!("val.{name}"), &format!("(*val.{name}).clone()"));
964                    format!("{name}: {unwrapped}")
965                }
966            } else {
967                conversion.to_string()
968            }
969        }
970        CoreWrapper::Bytes => {
971            // Bytes → Vec<u8> (or napi Buffer via From<Vec<u8>>): .to_vec().into()
972            // The TypeRef::Bytes field_conversion already emits the correct expression
973            // (`.to_vec().into()` non-optional, `.map(|v| v.to_vec().into())` optional).
974            // Detect those forms and pass through unchanged to avoid double conversion.
975            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
976                let already_converted_non_opt = expr == format!("val.{name}.to_vec().into()");
977                let already_converted_opt = expr == format!("val.{name}.map(|v| v.to_vec().into())");
978                if already_converted_non_opt || already_converted_opt {
979                    conversion.to_string()
980                } else if optional {
981                    format!("{name}: {expr}.map(|v| v.to_vec().into())")
982                } else if expr == format!("val.{name}") {
983                    format!("{name}: val.{name}.to_vec().into()")
984                } else {
985                    conversion.to_string()
986                }
987            } else {
988                conversion.to_string()
989            }
990        }
991        CoreWrapper::ArcMutex => {
992            // Arc<Mutex<T>> → T: lock and clone
993            if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
994                if optional {
995                    format!("{name}: {expr}.map(|v| v.lock().unwrap().clone().into())")
996                } else if expr == format!("val.{name}") {
997                    format!("{name}: val.{name}.lock().unwrap().clone().into()")
998                } else {
999                    conversion.to_string()
1000                }
1001            } else {
1002                conversion.to_string()
1003            }
1004        }
1005    }
1006}