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