Skip to main content

alef_codegen/conversions/
mod.rs

1mod binding_to_core;
2mod core_to_binding;
3mod enums;
4pub(crate) mod helpers;
5
6use ahash::AHashSet;
7
8/// Backend-specific configuration for From/field conversion generation.
9/// Enables shared code to handle all backend differences via parameters.
10#[derive(Default, Clone)]
11pub struct ConversionConfig<'a> {
12    /// Prefix for binding type names ("Js" for NAPI/WASM, "" for others).
13    pub type_name_prefix: &'a str,
14    /// U64/Usize/Isize need `as i64` casts (NAPI, PHP — JS/PHP lack native u64).
15    pub cast_large_ints_to_i64: bool,
16    /// Enum names mapped to String in the binding layer (PHP only).
17    /// Named fields referencing these use `format!("{:?}")` in core→binding.
18    pub enum_string_names: Option<&'a AHashSet<String>>,
19    /// Map types use JsValue in the binding layer (WASM only).
20    /// When true, Map fields use `serde_wasm_bindgen` for conversion instead of
21    /// iterator-based collect patterns (JsValue is not iterable).
22    pub map_uses_jsvalue: bool,
23    /// When true, f32 is mapped to f64 (NAPI only — JS has no f32).
24    pub cast_f32_to_f64: bool,
25    /// When true, non-optional fields on defaultable types are wrapped in Option<T>
26    /// in the binding struct and need `.unwrap_or_default()` in binding→core From.
27    /// Used by NAPI to make JS-facing structs fully optional.
28    pub optionalize_defaults: bool,
29    /// When true, Json (serde_json::Value) fields are mapped to String in the binding layer.
30    /// Core→binding uses `.to_string()`, binding→core uses `Default::default()` (lossy).
31    /// Used by PHP where serde_json::Value can't cross the extension boundary.
32    pub json_to_string: bool,
33    /// When true, Json fields stay as `serde_json::Value` in the binding layer (no wrapping).
34    /// Core↔binding conversions are identity since both sides hold the same type.
35    /// Used by NAPI (with `serde-json` feature) so JS callers can pass arbitrary objects
36    /// directly without first stringifying them.
37    pub json_as_value: bool,
38    /// When true, add synthetic metadata field conversion for ConversionResult.
39    /// Only NAPI backend sets this (it adds metadata field to the struct).
40    pub include_cfg_metadata: bool,
41    /// When true, non-optional Duration fields on `has_default` types are stored as
42    /// `Option<u64>` in the binding struct.  The From conversion uses the builder
43    /// pattern so that `None` falls back to the core type's `Default` implementation
44    /// (giving the real default, e.g. `Duration::from_secs(30)`) instead of `Duration::ZERO`.
45    /// Used by PyO3 to prevent validation failures when `request_timeout` is unset.
46    pub option_duration_on_defaults: bool,
47    /// When true, binding enums include data variant fields (Magnus).
48    /// When false (default), binding enums are unit-only and data is lost in conversion.
49    pub binding_enums_have_data: bool,
50    /// Type names excluded from the binding layer. Fields referencing these types
51    /// are skipped in the binding struct and defaulted in From conversions.
52    /// Used by WASM to handle types excluded due to native dependency requirements.
53    pub exclude_types: &'a [String],
54    /// When true, Vec<Named> fields are stored as JSON strings in the binding layer.
55    /// Core→binding uses `serde_json::to_string`, binding→core uses `serde_json::from_str`.
56    /// Used by Magnus (Ruby) where Vec<Named> cannot cross the FFI boundary directly and
57    /// is collapsed to String by `field_type_for_serde`'s catch-all arm.
58    pub vec_named_to_string: bool,
59    /// When true, all Map(K, V) fields are stored as a plain `String` in the binding layer.
60    /// Core→binding uses `format!("{:?}", val.field)`, binding→core uses `Default::default()` (lossy).
61    /// Used by Rustler (Elixir NIFs) where `HashMap` cannot cross the NIF boundary directly.
62    pub map_as_string: bool,
63    /// Set of opaque type names in the binding layer.
64    /// When a field has `CoreWrapper::Arc` and its type is an opaque Named type,
65    /// the binding wrapper holds `inner: Arc<CoreT>` and the conversion must extract
66    /// `.inner` directly instead of calling `.into()` + wrapping in `Arc::new`.
67    pub opaque_types: Option<&'a AHashSet<String>>,
68    /// Type names that should use `Default::default()` in the binding→core From impl.
69    /// Used by PHP to skip bridge type fields (e.g., VisitorHandle) that can't be
70    /// auto-converted via Into and are always handled by the bridge machinery instead.
71    pub from_binding_skip_types: &'a [String],
72    /// When `core_crate_override` is set for a language, the IR's `rust_path` values
73    /// still contain the original source crate prefix (e.g. `mylib_core::Method`).
74    /// This field remaps those paths: `(original_crate_name, override_crate_name)`.
75    /// When set, any `rust_path` whose leading crate segment equals `original_crate_name`
76    /// is rewritten to use `override_crate_name` instead.
77    /// Example: `Some(("mylib_core", "mylib_http"))` rewrites
78    /// `mylib_core::Method` → `mylib_http::Method`.
79    pub source_crate_remaps: &'a [(&'a str, &'a str)],
80    /// Per-field binding name overrides.  Key is `"TypeName.field_name"` (using the original
81    /// IR field name); value is the binding struct's actual Rust field name (e.g. `"class_"`).
82    /// Used when a field name is a reserved keyword in the target language and must be escaped
83    /// in the binding struct (e.g. `class` → `class_`).
84    ///
85    /// When present, `val.<binding_name>` is used for binding-side access and the original
86    /// `field_name` is used for core-side access (struct literal and assignment targets).
87    pub binding_field_renames: Option<&'a std::collections::HashMap<String, String>>,
88    /// When true, U8/U16/U32 (and their signed counterparts I8/I16) need `as i32` casts.
89    /// extendr maps all small integers to R's native integer type (i32), so binding→core
90    /// conversions must cast back to the original unsigned/narrow types.
91    pub cast_uints_to_i32: bool,
92    /// When true, U64/Usize/Isize are mapped to f64 (R's native double type) rather than i64.
93    /// extendr uses f64 for large integers because R has no native 64-bit integer type.
94    /// Binding→core: `as usize`/`as u64` casts; core→binding: `as f64` casts.
95    pub cast_large_ints_to_f64: bool,
96}
97
98impl<'a> ConversionConfig<'a> {
99    /// Look up the binding struct field name for a given type and IR field name.
100    ///
101    /// Returns the escaped name (e.g. `"class_"`) when the field was renamed due to a
102    /// reserved keyword conflict, or the original `field_name` when no rename applies.
103    pub fn binding_field_name<'b>(&self, type_name: &str, field_name: &'b str) -> &'b str
104    where
105        'a: 'b,
106    {
107        // &'b str: we return either the original (which has lifetime 'b from the parameter)
108        // or a &str from the HashMap (which would have lifetime 'a). Since 'a: 'b we can
109        // return either. But Rust's lifetime inference won't let us return `&'a str` from a
110        // `&'b str` parameter without unsafe. Use a helper that returns an owned String instead.
111        let _ = type_name;
112        field_name
113    }
114
115    /// Like `binding_field_name` but returns an owned `String`, suitable for use in
116    /// format strings and string interpolation.
117    pub fn binding_field_name_owned(&self, type_name: &str, field_name: &str) -> String {
118        if let Some(map) = self.binding_field_renames {
119            let key = format!("{type_name}.{field_name}");
120            if let Some(renamed) = map.get(&key) {
121                return renamed.clone();
122            }
123        }
124        field_name.to_string()
125    }
126}
127
128// Re-export all public items so callers continue to use `conversions::foo`.
129pub use binding_to_core::{
130    apply_core_wrapper_to_core, field_conversion_to_core, field_conversion_to_core_cfg, gen_from_binding_to_core,
131    gen_from_binding_to_core_cfg,
132};
133pub use core_to_binding::{
134    field_conversion_from_core, field_conversion_from_core_cfg, gen_from_core_to_binding, gen_from_core_to_binding_cfg,
135};
136pub use enums::{
137    gen_enum_from_binding_to_core, gen_enum_from_binding_to_core_cfg, gen_enum_from_core_to_binding,
138    gen_enum_from_core_to_binding_cfg,
139};
140pub use helpers::{
141    apply_crate_remaps, binding_to_core_match_arm, build_type_path_map, can_generate_conversion,
142    can_generate_enum_conversion, can_generate_enum_conversion_from_core, convertible_types, core_enum_path,
143    core_enum_path_remapped, core_to_binding_convertible_types, core_to_binding_match_arm, core_type_path,
144    core_type_path_remapped, field_references_excluded_type, has_sanitized_fields, input_type_names, is_tuple_variant,
145    resolve_named_path,
146};
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use alef_core::ir::*;
152
153    fn simple_type() -> TypeDef {
154        TypeDef {
155            name: "Config".to_string(),
156            rust_path: "my_crate::Config".to_string(),
157            original_rust_path: String::new(),
158            fields: vec![
159                FieldDef {
160                    name: "name".into(),
161                    ty: TypeRef::String,
162                    optional: false,
163                    default: None,
164                    doc: String::new(),
165                    sanitized: false,
166                    is_boxed: false,
167                    type_rust_path: None,
168                    cfg: None,
169                    typed_default: None,
170                    core_wrapper: CoreWrapper::None,
171                    vec_inner_core_wrapper: CoreWrapper::None,
172                    newtype_wrapper: None,
173                    serde_rename: None,
174                },
175                FieldDef {
176                    name: "timeout".into(),
177                    ty: TypeRef::Primitive(PrimitiveType::U64),
178                    optional: true,
179                    default: None,
180                    doc: String::new(),
181                    sanitized: false,
182                    is_boxed: false,
183                    type_rust_path: None,
184                    cfg: None,
185                    typed_default: None,
186                    core_wrapper: CoreWrapper::None,
187                    vec_inner_core_wrapper: CoreWrapper::None,
188                    newtype_wrapper: None,
189                    serde_rename: None,
190                },
191                FieldDef {
192                    name: "backend".into(),
193                    ty: TypeRef::Named("Backend".into()),
194                    optional: true,
195                    default: None,
196                    doc: String::new(),
197                    sanitized: false,
198                    is_boxed: false,
199                    type_rust_path: None,
200                    cfg: None,
201                    typed_default: None,
202                    core_wrapper: CoreWrapper::None,
203                    vec_inner_core_wrapper: CoreWrapper::None,
204                    newtype_wrapper: None,
205                    serde_rename: None,
206                },
207            ],
208            methods: vec![],
209            is_opaque: false,
210            is_clone: true,
211            is_copy: false,
212            is_trait: false,
213            has_default: false,
214            has_stripped_cfg_fields: false,
215            is_return_type: false,
216            serde_rename_all: None,
217            has_serde: false,
218            super_traits: vec![],
219            doc: String::new(),
220            cfg: None,
221        }
222    }
223
224    fn simple_enum() -> EnumDef {
225        EnumDef {
226            name: "Backend".to_string(),
227            rust_path: "my_crate::Backend".to_string(),
228            original_rust_path: String::new(),
229            variants: vec![
230                EnumVariant {
231                    name: "Cpu".into(),
232                    fields: vec![],
233                    is_tuple: false,
234                    doc: String::new(),
235                    is_default: false,
236                    serde_rename: None,
237                },
238                EnumVariant {
239                    name: "Gpu".into(),
240                    fields: vec![],
241                    is_tuple: false,
242                    doc: String::new(),
243                    is_default: false,
244                    serde_rename: None,
245                },
246            ],
247            doc: String::new(),
248            cfg: None,
249            is_copy: false,
250            has_serde: false,
251            serde_tag: None,
252            serde_untagged: false,
253            serde_rename_all: None,
254        }
255    }
256
257    #[test]
258    fn test_from_binding_to_core() {
259        let typ = simple_type();
260        let result = gen_from_binding_to_core(&typ, "my_crate");
261        assert!(result.contains("impl From<Config> for my_crate::Config"));
262        assert!(result.contains("name: val.name"));
263        assert!(result.contains("timeout: val.timeout"));
264        assert!(result.contains("backend: val.backend.map(Into::into)"));
265    }
266
267    #[test]
268    fn test_from_core_to_binding() {
269        let typ = simple_type();
270        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
271        assert!(result.contains("impl From<my_crate::Config> for Config"));
272    }
273
274    #[test]
275    fn test_enum_from_binding_to_core() {
276        let enum_def = simple_enum();
277        let result = gen_enum_from_binding_to_core(&enum_def, "my_crate");
278        assert!(result.contains("impl From<Backend> for my_crate::Backend"));
279        assert!(result.contains("Backend::Cpu => Self::Cpu"));
280        assert!(result.contains("Backend::Gpu => Self::Gpu"));
281    }
282
283    #[test]
284    fn test_enum_from_core_to_binding() {
285        let enum_def = simple_enum();
286        let result = gen_enum_from_core_to_binding(&enum_def, "my_crate");
287        assert!(result.contains("impl From<my_crate::Backend> for Backend"));
288        assert!(result.contains("my_crate::Backend::Cpu => Self::Cpu"));
289        assert!(result.contains("my_crate::Backend::Gpu => Self::Gpu"));
290    }
291
292    #[test]
293    fn test_from_binding_to_core_with_cfg_gated_field() {
294        // Create a type with a cfg-gated field
295        let mut typ = simple_type();
296        typ.has_stripped_cfg_fields = true;
297        typ.fields.push(FieldDef {
298            name: "layout".into(),
299            ty: TypeRef::String,
300            optional: false,
301            default: None,
302            doc: String::new(),
303            sanitized: false,
304            is_boxed: false,
305            type_rust_path: None,
306            cfg: Some("feature = \"layout-detection\"".into()),
307            typed_default: None,
308            core_wrapper: CoreWrapper::None,
309            vec_inner_core_wrapper: CoreWrapper::None,
310            newtype_wrapper: None,
311            serde_rename: None,
312        });
313
314        let result = gen_from_binding_to_core(&typ, "my_crate");
315
316        // The impl should exist
317        assert!(result.contains("impl From<Config> for my_crate::Config"));
318        // Regular fields should be present
319        assert!(result.contains("name: val.name"));
320        assert!(result.contains("timeout: val.timeout"));
321        // cfg-gated field should NOT be accessed from val (it doesn't exist in binding struct)
322        assert!(!result.contains("layout: val.layout"));
323        // But ..Default::default() should be present to fill cfg-gated fields
324        assert!(result.contains("..Default::default()"));
325    }
326
327    #[test]
328    fn test_from_core_to_binding_with_cfg_gated_field() {
329        // Create a type with a cfg-gated field
330        let mut typ = simple_type();
331        typ.fields.push(FieldDef {
332            name: "layout".into(),
333            ty: TypeRef::String,
334            optional: false,
335            default: None,
336            doc: String::new(),
337            sanitized: false,
338            is_boxed: false,
339            type_rust_path: None,
340            cfg: Some("feature = \"layout-detection\"".into()),
341            typed_default: None,
342            core_wrapper: CoreWrapper::None,
343            vec_inner_core_wrapper: CoreWrapper::None,
344            newtype_wrapper: None,
345            serde_rename: None,
346        });
347
348        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
349
350        // The impl should exist
351        assert!(result.contains("impl From<my_crate::Config> for Config"));
352        // Regular fields should be present
353        assert!(result.contains("name: val.name"));
354        // cfg-gated field should NOT be in the struct literal
355        assert!(!result.contains("layout:"));
356    }
357
358    #[test]
359    fn test_field_conversion_from_core_map_named_non_optional() {
360        // Map<K, Named> non-optional: each value needs .into() core→binding
361        let result = field_conversion_from_core(
362            "tags",
363            &TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Named("Tag".into()))),
364            false,
365            false,
366            &AHashSet::new(),
367        );
368        assert_eq!(
369            result,
370            "tags: val.tags.into_iter().map(|(k, v)| (k, v.into())).collect()"
371        );
372    }
373
374    #[test]
375    fn test_field_conversion_from_core_option_map_named() {
376        // Option<Map<K, Named>>: .map() wrapper + per-element .into()
377        let result = field_conversion_from_core(
378            "tags",
379            &TypeRef::Optional(Box::new(TypeRef::Map(
380                Box::new(TypeRef::String),
381                Box::new(TypeRef::Named("Tag".into())),
382            ))),
383            false,
384            false,
385            &AHashSet::new(),
386        );
387        assert_eq!(
388            result,
389            "tags: val.tags.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
390        );
391    }
392
393    #[test]
394    fn test_field_conversion_from_core_vec_named_non_optional() {
395        // Vec<Named> non-optional: each element needs .into() core→binding
396        let result = field_conversion_from_core(
397            "items",
398            &TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))),
399            false,
400            false,
401            &AHashSet::new(),
402        );
403        assert_eq!(result, "items: val.items.into_iter().map(Into::into).collect()");
404    }
405
406    #[test]
407    fn test_field_conversion_from_core_option_vec_named() {
408        // Option<Vec<Named>>: .map() wrapper + per-element .into()
409        let result = field_conversion_from_core(
410            "items",
411            &TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Item".into()))))),
412            false,
413            false,
414            &AHashSet::new(),
415        );
416        assert_eq!(
417            result,
418            "items: val.items.map(|v| v.into_iter().map(Into::into).collect())"
419        );
420    }
421
422    #[test]
423    fn test_field_conversion_to_core_option_map_named_applies_per_value_into() {
424        // Bug A1 regression: Option<Map<K, Named>> must apply per-value .into() so that
425        // binding-side wrapper types (e.g. PyO3 / Magnus structs) are converted correctly.
426        let result = field_conversion_to_core(
427            "patterns",
428            &TypeRef::Map(
429                Box::new(TypeRef::String),
430                Box::new(TypeRef::Named("ExtractionPattern".into())),
431            ),
432            true,
433        );
434        assert!(
435            result.contains("m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()"),
436            "expected per-value v.into() in optional Map<Named> conversion, got: {result}"
437        );
438        assert_eq!(
439            result,
440            "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect())"
441        );
442    }
443
444    #[test]
445    fn test_gen_optionalized_field_to_core_ir_optional_map_named_preserves_option() {
446        // Bug A3 regression: when field_is_ir_optional=true, gen_optionalized_field_to_core must
447        // preserve the Option layer via .map(|m| …) instead of dropping it with unwrap_or_default().
448        use super::binding_to_core::gen_optionalized_field_to_core;
449        let config = ConversionConfig::default();
450        let result = gen_optionalized_field_to_core(
451            "patterns",
452            &TypeRef::Map(
453                Box::new(TypeRef::String),
454                Box::new(TypeRef::Named("ExtractionPattern".into())),
455            ),
456            &config,
457            true,
458        );
459        assert!(
460            result.contains("m.into_iter().map(|(k, v)| (k, v.into())).collect()"),
461            "expected per-value v.into() in ir-optional Map<Named> conversion, got: {result}"
462        );
463        assert_eq!(
464            result,
465            "patterns: val.patterns.map(|m| m.into_iter().map(|(k, v)| (k, v.into())).collect())"
466        );
467    }
468
469    #[test]
470    fn test_optionalized_defaultable_struct_uses_core_default_as_base() {
471        let mut typ = simple_type();
472        typ.has_default = true;
473        typ.fields = vec![
474            FieldDef {
475                name: "language".into(),
476                ty: TypeRef::String,
477                optional: false,
478                default: None,
479                doc: String::new(),
480                sanitized: false,
481                is_boxed: false,
482                type_rust_path: None,
483                cfg: None,
484                typed_default: None,
485                core_wrapper: CoreWrapper::Cow,
486                vec_inner_core_wrapper: CoreWrapper::None,
487                newtype_wrapper: None,
488                serde_rename: None,
489            },
490            FieldDef {
491                name: "structure".into(),
492                ty: TypeRef::Primitive(PrimitiveType::Bool),
493                optional: false,
494                default: None,
495                doc: String::new(),
496                sanitized: false,
497                is_boxed: false,
498                type_rust_path: None,
499                cfg: None,
500                typed_default: None,
501                core_wrapper: CoreWrapper::None,
502                vec_inner_core_wrapper: CoreWrapper::None,
503                newtype_wrapper: None,
504                serde_rename: None,
505            },
506        ];
507        let config = ConversionConfig {
508            type_name_prefix: "Js",
509            optionalize_defaults: true,
510            ..ConversionConfig::default()
511        };
512
513        let result = gen_from_binding_to_core_cfg(&typ, "my_crate", &config);
514
515        assert!(result.contains("let mut __result = my_crate::Config::default();"));
516        assert!(result.contains("if let Some(__v) = val.language { __result.language = __v.into(); }"));
517        assert!(result.contains("if let Some(__v) = val.structure { __result.structure = __v; }"));
518        assert!(!result.contains("unwrap_or_default()"));
519    }
520
521    fn arc_field_type(field: FieldDef) -> TypeDef {
522        TypeDef {
523            name: "State".to_string(),
524            rust_path: "my_crate::State".to_string(),
525            original_rust_path: String::new(),
526            fields: vec![field],
527            methods: vec![],
528            is_opaque: false,
529            is_clone: true,
530            is_copy: false,
531            is_trait: false,
532            has_default: false,
533            has_stripped_cfg_fields: false,
534            is_return_type: false,
535            serde_rename_all: None,
536            has_serde: false,
537            super_traits: vec![],
538            doc: String::new(),
539            cfg: None,
540        }
541    }
542
543    fn arc_field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
544        FieldDef {
545            name: name.into(),
546            ty,
547            optional,
548            default: None,
549            doc: String::new(),
550            sanitized: false,
551            is_boxed: false,
552            type_rust_path: None,
553            cfg: None,
554            typed_default: None,
555            core_wrapper: CoreWrapper::Arc,
556            vec_inner_core_wrapper: CoreWrapper::None,
557            newtype_wrapper: None,
558            serde_rename: None,
559        }
560    }
561
562    /// Regression: Option<Arc<serde_json::Value>> must not chain `(*v).clone().into()`
563    /// on top of `as_ref().map(ToString::to_string)`, which would emit invalid
564    /// `(*String).clone()` (str: !Clone).
565    #[test]
566    fn test_arc_json_option_field_no_double_chain() {
567        let typ = arc_field_type(arc_field("registered_spec", TypeRef::Json, true));
568        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
569        assert!(
570            result.contains("val.registered_spec.as_ref().map(ToString::to_string)"),
571            "expected as_ref().map(ToString::to_string) for Option<Arc<Value>>, got: {result}"
572        );
573        assert!(
574            !result.contains("map(ToString::to_string).map("),
575            "must not chain a second map() on top of ToString::to_string, got: {result}"
576        );
577    }
578
579    /// Non-optional Arc<Value>: `(*val.X).clone().to_string()` is valid (Value: Clone).
580    #[test]
581    fn test_arc_json_non_optional_field() {
582        let typ = arc_field_type(arc_field("spec", TypeRef::Json, false));
583        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
584        assert!(
585            result.contains("(*val.spec).clone().to_string()"),
586            "expected (*val.spec).clone().to_string() for Arc<Value>, got: {result}"
587        );
588    }
589
590    /// Option<Arc<String>>: simple passthrough → `.map(|v| (*v).clone().into())` is valid
591    /// (String: Clone). Verifies the simple_passthrough branch is preserved.
592    #[test]
593    fn test_arc_string_option_field_passthrough() {
594        let typ = arc_field_type(arc_field("label", TypeRef::String, true));
595        let result = gen_from_core_to_binding(&typ, "my_crate", &AHashSet::new());
596        assert!(
597            result.contains("val.label.map(|v| (*v).clone().into())"),
598            "expected .map(|v| (*v).clone().into()) for Option<Arc<String>>, got: {result}"
599        );
600    }
601}