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    }
100}
101
102/// Synthesize type for a primitive value
103fn synth_primitive(prim: &PrimitiveValue) -> SynthType {
104    match prim {
105        PrimitiveValue::Null => SynthType::Null,
106        PrimitiveValue::Bool(_) => SynthType::Boolean,
107        PrimitiveValue::Integer(_) => SynthType::Integer,
108        PrimitiveValue::F32(_) | PrimitiveValue::F64(_) => SynthType::Float,
109        PrimitiveValue::Text(text) => SynthType::Text(synth_text_language(&text.language)),
110    }
111}
112
113/// Extract language from Text value
114fn synth_text_language(lang: &Language) -> Option<String> {
115    match lang {
116        Language::Implicit => None,
117        Language::Plaintext => Some("plaintext".to_string()),
118        Language::Other(lang) => Some(lang.to_string()),
119    }
120}
121
122/// Convert ObjectKey to a field name string
123///
124/// For string keys, returns the raw string.
125/// For other key types, uses the Display representation.
126fn object_key_to_field_name(key: &ObjectKey) -> String {
127    match key {
128        ObjectKey::String(s) => s.clone(),
129        ObjectKey::Number(n) => n.to_string(),
130        ObjectKey::Tuple(t) => format!("{:?}", t), // Fallback for tuple keys
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use eure_document::document::node::{NodeArray, NodeMap};
138    use eure_document::eure;
139    use eure_document::text::Text;
140    use eure_document::value::ObjectKey;
141    use num_bigint::BigInt;
142
143    #[test]
144    fn test_synth_primitives() {
145        let doc = EureDocument::new_primitive(PrimitiveValue::Null);
146        assert_eq!(synth(&doc, doc.get_root_id()), SynthType::Null);
147
148        let doc = EureDocument::new_primitive(PrimitiveValue::Bool(true));
149        assert_eq!(synth(&doc, doc.get_root_id()), SynthType::Boolean);
150
151        let doc = EureDocument::new_primitive(PrimitiveValue::Integer(BigInt::from(42)));
152        assert_eq!(synth(&doc, doc.get_root_id()), SynthType::Integer);
153
154        let doc = EureDocument::new_primitive(PrimitiveValue::F64(2.5));
155        assert_eq!(synth(&doc, doc.get_root_id()), SynthType::Float);
156
157        let doc = EureDocument::new_primitive(PrimitiveValue::Text(Text::plaintext("hello")));
158        assert_eq!(
159            synth(&doc, doc.get_root_id()),
160            SynthType::Text(Some("plaintext".to_string()))
161        );
162    }
163
164    #[test]
165    fn test_synth_empty_array() {
166        let doc = eure!({ arr = [] });
167        let root = doc.node(doc.get_root_id());
168        let arr_id = root
169            .as_map()
170            .unwrap()
171            .get_node_id(&ObjectKey::String("arr".into()))
172            .unwrap();
173        assert_eq!(
174            synth(&doc, arr_id),
175            SynthType::Array(Box::new(SynthType::Any))
176        );
177    }
178
179    #[test]
180    fn test_synth_homogeneous_array() {
181        let doc = eure!({ arr = [1, 2, 3] });
182        let root = doc.node(doc.get_root_id());
183        let arr_id = root
184            .as_map()
185            .unwrap()
186            .get_node_id(&ObjectKey::String("arr".into()))
187            .unwrap();
188        assert_eq!(
189            synth(&doc, arr_id),
190            SynthType::Array(Box::new(SynthType::Integer))
191        );
192    }
193
194    #[test]
195    fn test_synth_heterogeneous_array() {
196        let doc = eure!({ arr = [1, "hello"] });
197        let root = doc.node(doc.get_root_id());
198        let arr_id = root
199            .as_map()
200            .unwrap()
201            .get_node_id(&ObjectKey::String("arr".into()))
202            .unwrap();
203        assert_eq!(
204            synth(&doc, arr_id),
205            SynthType::Array(Box::new(SynthType::Union(SynthUnion {
206                variants: vec![
207                    SynthType::Integer,
208                    SynthType::Text(Some("plaintext".to_string()))
209                ]
210            })))
211        );
212    }
213
214    #[test]
215    fn test_synth_tuple() {
216        let doc = eure!({ tup = (1, "hello", true) });
217        let root = doc.node(doc.get_root_id());
218        let tup_id = root
219            .as_map()
220            .unwrap()
221            .get_node_id(&ObjectKey::String("tup".into()))
222            .unwrap();
223        assert_eq!(
224            synth(&doc, tup_id),
225            SynthType::Tuple(vec![
226                SynthType::Integer,
227                SynthType::Text(Some("plaintext".to_string())),
228                SynthType::Boolean,
229            ])
230        );
231    }
232
233    #[test]
234    fn test_synth_record() {
235        let doc = eure!({
236            name = "Alice"
237            age = 30
238        });
239        let ty = synth(&doc, doc.get_root_id());
240        let expected = SynthType::Record(SynthRecord::new([
241            (
242                "name".to_string(),
243                SynthField::required(SynthType::Text(Some("plaintext".to_string()))),
244            ),
245            ("age".to_string(), SynthField::required(SynthType::Integer)),
246        ]));
247        assert_eq!(ty, expected);
248    }
249
250    #[test]
251    fn test_synth_array_of_records_union() {
252        // Build [ { a = 1 }, { a = "x", b = "y" } ] programmatically
253        let mut doc = EureDocument::new_empty();
254
255        // Create first record: { a = 1 }
256        let a1_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(
257            1,
258        ))));
259        let rec1_id = doc.create_node(NodeValue::Map(NodeMap::from_iter([(
260            ObjectKey::String("a".into()),
261            a1_id,
262        )])));
263
264        // Create second record: { a = "x", b = "y" }
265        let a2_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext(
266            "x",
267        ))));
268        let b2_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext(
269            "y",
270        ))));
271        let rec2_id = doc.create_node(NodeValue::Map(NodeMap::from_iter([
272            (ObjectKey::String("a".into()), a2_id),
273            (ObjectKey::String("b".into()), b2_id),
274        ])));
275
276        // Create array
277        let arr_id = doc.create_node(NodeValue::Array(NodeArray::from_vec(vec![
278            rec1_id, rec2_id,
279        ])));
280
281        let ty = synth(&doc, arr_id);
282
283        // Different shapes form a union of records
284        let expected = SynthType::Array(Box::new(SynthType::Union(SynthUnion {
285            variants: vec![
286                SynthType::Record(SynthRecord::new([(
287                    "a".to_string(),
288                    SynthField::required(SynthType::Integer),
289                )])),
290                SynthType::Record(SynthRecord::new([
291                    (
292                        "a".to_string(),
293                        SynthField::required(SynthType::Text(Some("plaintext".to_string()))),
294                    ),
295                    (
296                        "b".to_string(),
297                        SynthField::required(SynthType::Text(Some("plaintext".to_string()))),
298                    ),
299                ])),
300            ],
301        })));
302        assert_eq!(ty, expected);
303    }
304
305    #[test]
306    fn test_synth_nested() {
307        let doc = eure!({
308            items = [1, 2]
309            meta {
310                count = 2
311            }
312        });
313        let ty = synth(&doc, doc.get_root_id());
314        let expected = SynthType::Record(SynthRecord::new([
315            (
316                "items".to_string(),
317                SynthField::required(SynthType::Array(Box::new(SynthType::Integer))),
318            ),
319            (
320                "meta".to_string(),
321                SynthField::required(SynthType::Record(SynthRecord::new([(
322                    "count".to_string(),
323                    SynthField::required(SynthType::Integer),
324                )]))),
325            ),
326        ]));
327        assert_eq!(ty, expected);
328    }
329
330    #[test]
331    fn test_synth_hole_absorbed() {
332        // Build [1, !, 3] programmatically (hole should be absorbed)
333        let mut doc = EureDocument::new_empty();
334        let i1 = doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(
335            1,
336        ))));
337        let hole = doc.create_node(NodeValue::Hole(None));
338        let i3 = doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(
339            3,
340        ))));
341        let arr_id = doc.create_node(NodeValue::Array(NodeArray::from_vec(vec![i1, hole, i3])));
342
343        assert_eq!(
344            synth(&doc, arr_id),
345            SynthType::Array(Box::new(SynthType::Integer))
346        );
347    }
348
349    #[test]
350    fn test_synth_same_shape_records_merge() {
351        // Build [ { a = 1 }, { a = "x" } ] - same shape, different field types
352        let mut doc = EureDocument::new_empty();
353
354        let a1_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(
355            1,
356        ))));
357        let rec1_id = doc.create_node(NodeValue::Map(NodeMap::from_iter([(
358            ObjectKey::String("a".into()),
359            a1_id,
360        )])));
361
362        let a2_id = doc.create_node(NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext(
363            "x",
364        ))));
365        let rec2_id = doc.create_node(NodeValue::Map(NodeMap::from_iter([(
366            ObjectKey::String("a".into()),
367            a2_id,
368        )])));
369
370        let arr_id = doc.create_node(NodeValue::Array(NodeArray::from_vec(vec![
371            rec1_id, rec2_id,
372        ])));
373
374        // Same shape records should merge, resulting in Record { a: Integer | Text }
375        let expected = SynthType::Array(Box::new(SynthType::Record(SynthRecord::new([(
376            "a".to_string(),
377            SynthField::required(SynthType::Union(SynthUnion {
378                variants: vec![
379                    SynthType::Integer,
380                    SynthType::Text(Some("plaintext".to_string())),
381                ],
382            })),
383        )]))));
384        assert_eq!(synth(&doc, arr_id), expected);
385    }
386}