Skip to main content

eure_schema/
synth.rs

1//! Type Synthesis for Eure Documents
2//!
3//! This module infers types from Eure document values without requiring a schema.
4//! The synthesized types can be used for:
5//! - Generating schema definitions from example data
6//! - Type checking across multiple files
7//! - Editor tooling (hover types, completions)
8//!
9//! # Example
10//!
11//! ```rust,ignore
12//! use eure_document::document::{EureDocument, NodeId};
13//! use eure_schema::synth::{synth, SynthType};
14//!
15//! let doc = eure!({ name = "Alice", age = 30 });
16//! let ty = synth(&doc, doc.get_root_id());
17//! // ty = Record { name: Text, age: Integer }
18//! ```
19//!
20//! # Unification
21//!
22//! When synthesizing arrays, element types are unified:
23//!
24//! ```rust,ignore
25//! // [ { a = 1 }, { a = "x", b = "y" } ]
26//! // Result: Array<{ a: Integer } | { a: Text, b: Text }>
27//! ```
28//!
29//! Holes are absorbed during unification:
30//!
31//! ```rust,ignore
32//! // [1, !, 3]
33//! // Result: Array<Integer>  (not Array<Integer | Hole>)
34//! ```
35
36mod types;
37mod unify;
38
39pub use types::*;
40pub use unify::unify;
41
42use eure_document::document::node::NodeValue;
43use eure_document::document::{EureDocument, NodeId};
44use eure_document::text::Language;
45use eure_document::value::{ObjectKey, PrimitiveValue};
46
47/// Synthesize a type from a document node.
48///
49/// This function recursively traverses the document structure and infers
50/// the most specific type for each value.
51///
52/// # Arguments
53///
54/// * `doc` - The Eure document containing the node
55/// * `node_id` - The node to synthesize a type for
56///
57/// # Returns
58///
59/// The synthesized type for the node
60pub fn synth(doc: &EureDocument, node_id: NodeId) -> SynthType {
61    let node = doc.node(node_id);
62
63    match &node.content {
64        NodeValue::Hole(ident) => SynthType::Hole(ident.clone()),
65
66        NodeValue::Primitive(prim) => synth_primitive(prim),
67
68        NodeValue::Array(arr) => {
69            if arr.is_empty() {
70                SynthType::Array(Box::new(SynthType::Any))
71            } else {
72                let element_types: Vec<_> = arr.iter().map(|&id| synth(doc, id)).collect();
73                let unified = element_types
74                    .into_iter()
75                    .reduce(unify)
76                    .unwrap_or(SynthType::Any);
77                SynthType::Array(Box::new(unified))
78            }
79        }
80
81        NodeValue::Tuple(tuple) => {
82            let element_types: Vec<_> = tuple.iter().map(|&id| synth(doc, id)).collect();
83            SynthType::Tuple(element_types)
84        }
85
86        NodeValue::Map(map) => {
87            if map.is_empty() {
88                SynthType::Record(SynthRecord::empty())
89            } else {
90                let mut fields = Vec::with_capacity(map.len());
91                for (key, &value_id) in map.iter() {
92                    let field_name = object_key_to_field_name(key);
93                    let field_type = synth(doc, value_id);
94                    fields.push((field_name, SynthField::required(field_type)));
95                }
96                SynthType::Record(SynthRecord::new(fields))
97            }
98        }
99        NodeValue::PartialMap(_) => SynthType::Any,
100    }
101}
102
103/// Synthesize type for a primitive value
104fn synth_primitive(prim: &PrimitiveValue) -> SynthType {
105    match prim {
106        PrimitiveValue::Null => SynthType::Null,
107        PrimitiveValue::Bool(_) => SynthType::Boolean,
108        PrimitiveValue::Integer(_) => SynthType::Integer,
109        PrimitiveValue::F32(_) | PrimitiveValue::F64(_) => SynthType::Float,
110        PrimitiveValue::Text(text) => SynthType::Text(synth_text_language(&text.language)),
111    }
112}
113
114/// Extract language from Text value
115fn synth_text_language(lang: &Language) -> Option<String> {
116    match lang {
117        Language::Implicit => None,
118        Language::Plaintext => Some("plaintext".to_string()),
119        Language::Other(lang) => Some(lang.to_string()),
120    }
121}
122
123/// Convert ObjectKey to a field name string
124///
125/// For string keys, returns the raw string.
126/// For other key types, uses the Display representation.
127fn object_key_to_field_name(key: &ObjectKey) -> String {
128    match key {
129        ObjectKey::String(s) => s.clone(),
130        ObjectKey::Number(n) => n.to_string(),
131        ObjectKey::Tuple(t) => format!("{:?}", t), // Fallback for tuple keys
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use eure_document::document::node::{NodeArray, NodeMap};
139    use eure_document::eure;
140    use eure_document::text::Text;
141    use eure_document::value::ObjectKey;
142    use num_bigint::BigInt;
143
144    #[test]
145    fn test_synth_primitives() {
146        let doc = EureDocument::new_primitive(PrimitiveValue::Null);
147        assert_eq!(synth(&doc, doc.get_root_id()), SynthType::Null);
148
149        let doc = EureDocument::new_primitive(PrimitiveValue::Bool(true));
150        assert_eq!(synth(&doc, doc.get_root_id()), SynthType::Boolean);
151
152        let doc = EureDocument::new_primitive(PrimitiveValue::Integer(BigInt::from(42)));
153        assert_eq!(synth(&doc, doc.get_root_id()), SynthType::Integer);
154
155        let doc = EureDocument::new_primitive(PrimitiveValue::F64(2.5));
156        assert_eq!(synth(&doc, doc.get_root_id()), SynthType::Float);
157
158        let doc = EureDocument::new_primitive(PrimitiveValue::Text(Text::plaintext("hello")));
159        assert_eq!(
160            synth(&doc, doc.get_root_id()),
161            SynthType::Text(Some("plaintext".to_string()))
162        );
163    }
164
165    #[test]
166    fn test_synth_empty_array() {
167        let doc = eure!({ arr = [] });
168        let root = doc.node(doc.get_root_id());
169        let arr_id = root
170            .as_map()
171            .unwrap()
172            .get_node_id(&ObjectKey::String("arr".into()))
173            .unwrap();
174        assert_eq!(
175            synth(&doc, arr_id),
176            SynthType::Array(Box::new(SynthType::Any))
177        );
178    }
179
180    #[test]
181    fn test_synth_homogeneous_array() {
182        let doc = eure!({ arr = [1, 2, 3] });
183        let root = doc.node(doc.get_root_id());
184        let arr_id = root
185            .as_map()
186            .unwrap()
187            .get_node_id(&ObjectKey::String("arr".into()))
188            .unwrap();
189        assert_eq!(
190            synth(&doc, arr_id),
191            SynthType::Array(Box::new(SynthType::Integer))
192        );
193    }
194
195    #[test]
196    fn test_synth_heterogeneous_array() {
197        let doc = eure!({ arr = [1, "hello"] });
198        let root = doc.node(doc.get_root_id());
199        let arr_id = root
200            .as_map()
201            .unwrap()
202            .get_node_id(&ObjectKey::String("arr".into()))
203            .unwrap();
204        assert_eq!(
205            synth(&doc, arr_id),
206            SynthType::Array(Box::new(SynthType::Union(SynthUnion {
207                variants: vec![
208                    SynthType::Integer,
209                    SynthType::Text(Some("plaintext".to_string()))
210                ]
211            })))
212        );
213    }
214
215    #[test]
216    fn test_synth_tuple() {
217        let doc = eure!({ tup = (1, "hello", true) });
218        let root = doc.node(doc.get_root_id());
219        let tup_id = root
220            .as_map()
221            .unwrap()
222            .get_node_id(&ObjectKey::String("tup".into()))
223            .unwrap();
224        assert_eq!(
225            synth(&doc, tup_id),
226            SynthType::Tuple(vec![
227                SynthType::Integer,
228                SynthType::Text(Some("plaintext".to_string())),
229                SynthType::Boolean,
230            ])
231        );
232    }
233
234    #[test]
235    fn test_synth_record() {
236        let doc = eure!({
237            name = "Alice"
238            age = 30
239        });
240        let ty = synth(&doc, doc.get_root_id());
241        let expected = SynthType::Record(SynthRecord::new([
242            (
243                "name".to_string(),
244                SynthField::required(SynthType::Text(Some("plaintext".to_string()))),
245            ),
246            ("age".to_string(), SynthField::required(SynthType::Integer)),
247        ]));
248        assert_eq!(ty, expected);
249    }
250
251    #[test]
252    fn test_synth_array_of_records_union() {
253        // Build [ { a = 1 }, { a = "x", b = "y" } ] programmatically
254        let mut doc = EureDocument::new_empty();
255
256        // Create first record: { a = 1 }
257        let a1_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(
258            1,
259        ))));
260        let rec1_id = doc.create_node(NodeValue::Map(NodeMap::from_iter([(
261            ObjectKey::String("a".into()),
262            a1_id,
263        )])));
264
265        // Create second record: { a = "x", b = "y" }
266        let a2_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext(
267            "x",
268        ))));
269        let b2_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext(
270            "y",
271        ))));
272        let rec2_id = doc.create_node(NodeValue::Map(NodeMap::from_iter([
273            (ObjectKey::String("a".into()), a2_id),
274            (ObjectKey::String("b".into()), b2_id),
275        ])));
276
277        // Create array
278        let arr_id = doc.create_node(NodeValue::Array(NodeArray::from_vec(vec![
279            rec1_id, rec2_id,
280        ])));
281
282        let ty = synth(&doc, arr_id);
283
284        // Different shapes form a union of records
285        let expected = SynthType::Array(Box::new(SynthType::Union(SynthUnion {
286            variants: vec![
287                SynthType::Record(SynthRecord::new([(
288                    "a".to_string(),
289                    SynthField::required(SynthType::Integer),
290                )])),
291                SynthType::Record(SynthRecord::new([
292                    (
293                        "a".to_string(),
294                        SynthField::required(SynthType::Text(Some("plaintext".to_string()))),
295                    ),
296                    (
297                        "b".to_string(),
298                        SynthField::required(SynthType::Text(Some("plaintext".to_string()))),
299                    ),
300                ])),
301            ],
302        })));
303        assert_eq!(ty, expected);
304    }
305
306    #[test]
307    fn test_synth_nested() {
308        let doc = eure!({
309            items = [1, 2]
310            meta {
311                count = 2
312            }
313        });
314        let ty = synth(&doc, doc.get_root_id());
315        let expected = SynthType::Record(SynthRecord::new([
316            (
317                "items".to_string(),
318                SynthField::required(SynthType::Array(Box::new(SynthType::Integer))),
319            ),
320            (
321                "meta".to_string(),
322                SynthField::required(SynthType::Record(SynthRecord::new([(
323                    "count".to_string(),
324                    SynthField::required(SynthType::Integer),
325                )]))),
326            ),
327        ]));
328        assert_eq!(ty, expected);
329    }
330
331    #[test]
332    fn test_synth_hole_absorbed() {
333        // Build [1, !, 3] programmatically (hole should be absorbed)
334        let mut doc = EureDocument::new_empty();
335        let i1 = doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(
336            1,
337        ))));
338        let hole = doc.create_node(NodeValue::Hole(None));
339        let i3 = doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(
340            3,
341        ))));
342        let arr_id = doc.create_node(NodeValue::Array(NodeArray::from_vec(vec![i1, hole, i3])));
343
344        assert_eq!(
345            synth(&doc, arr_id),
346            SynthType::Array(Box::new(SynthType::Integer))
347        );
348    }
349
350    #[test]
351    fn test_synth_same_shape_records_merge() {
352        // Build [ { a = 1 }, { a = "x" } ] - same shape, different field types
353        let mut doc = EureDocument::new_empty();
354
355        let a1_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(
356            1,
357        ))));
358        let rec1_id = doc.create_node(NodeValue::Map(NodeMap::from_iter([(
359            ObjectKey::String("a".into()),
360            a1_id,
361        )])));
362
363        let a2_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext(
364            "x",
365        ))));
366        let rec2_id = doc.create_node(NodeValue::Map(NodeMap::from_iter([(
367            ObjectKey::String("a".into()),
368            a2_id,
369        )])));
370
371        let arr_id = doc.create_node(NodeValue::Array(NodeArray::from_vec(vec![
372            rec1_id, rec2_id,
373        ])));
374
375        // Same shape records should merge, resulting in Record { a: Integer | Text }
376        let expected = SynthType::Array(Box::new(SynthType::Record(SynthRecord::new([(
377            "a".to_string(),
378            SynthField::required(SynthType::Union(SynthUnion {
379                variants: vec![
380                    SynthType::Integer,
381                    SynthType::Text(Some("plaintext".to_string())),
382                ],
383            })),
384        )]))));
385        assert_eq!(synth(&doc, arr_id), expected);
386    }
387}