Skip to main content

lemma/planning/
types.rs

1//! Type registry for managing custom type definitions and resolution
2//!
3//! This module provides the `TypeRegistry` which handles:
4//! - Registering user-defined types for each document
5//! - Resolving type hierarchies and inheritance chains
6//! - Detecting and preventing circular dependencies
7//! - Applying constraints to create final type specifications
8
9use crate::error::LemmaError;
10use crate::parsing::ast::{CommandArg, FactReference, TypeDef};
11use crate::planning::semantics::{self, LemmaType, TypeExtends, TypeSpecification};
12
13use std::collections::{HashMap, HashSet};
14
15/// Fully resolved types for a single document
16/// After resolution, all imports are inlined - documents are independent
17#[derive(Debug, Clone)]
18pub struct ResolvedDocumentTypes {
19    /// Named types: type_name -> fully resolved type
20    pub named_types: HashMap<String, LemmaType>,
21
22    /// Inline type definitions: fact reference -> fully resolved type
23    pub inline_type_definitions: HashMap<FactReference, LemmaType>,
24
25    /// Unit index: unit_name -> type that defines it
26    /// Built during resolution - if unit appears in multiple types, resolution fails
27    pub unit_index: HashMap<String, LemmaType>,
28}
29
30/// Registry for managing and resolving custom types
31///
32/// Types are organized per document and support inheritance through parent references.
33/// The registry handles cycle detection and accumulates constraints through the inheritance chain.
34#[derive(Debug, Clone)]
35pub struct TypeRegistry {
36    /// Named types per document: doc_name -> (type_name -> TypeDef)
37    /// Stores the raw definitions extracted from the AST
38    named_types: HashMap<String, HashMap<String, TypeDef>>,
39    /// Inline type definitions per document: doc_name -> (fact_reference -> TypeDef)
40    /// Stores inline type definitions keyed by their fact reference
41    inline_type_definitions: HashMap<String, HashMap<FactReference, TypeDef>>,
42    /// Source text per file attribute, used for error reporting.
43    /// Maps source attribute (e.g. filename) to the full source code string.
44    _sources: HashMap<String, String>,
45}
46
47impl TypeRegistry {
48    /// Create a new, empty registry with access to source text for error reporting.
49    pub fn new(sources: HashMap<String, String>) -> Self {
50        TypeRegistry {
51            named_types: HashMap::new(),
52            inline_type_definitions: HashMap::new(),
53            _sources: sources,
54        }
55    }
56
57    /// Register a user-defined type for a given document
58    pub fn register_type(&mut self, doc: &str, def: TypeDef) -> Result<(), LemmaError> {
59        let def_loc = def.source_location().clone();
60        match &def {
61            TypeDef::Regular { name, .. } | TypeDef::Import { name, .. } => {
62                // Named type
63                let doc_types = self.named_types.entry(doc.to_string()).or_default();
64
65                // Check if this type already exists
66                if doc_types.contains_key(name) {
67                    return Err(LemmaError::engine(
68                        format!("Type '{}' is already defined in document '{}'", name, doc),
69                        Some(def_loc.clone()),
70                        None::<String>,
71                    ));
72                }
73
74                // Store the type definition
75                doc_types.insert(name.clone(), def);
76            }
77            TypeDef::Inline { fact_ref, .. } => {
78                // Inline type definition
79                let doc_inline_types = self
80                    .inline_type_definitions
81                    .entry(doc.to_string())
82                    .or_default();
83
84                // Check if this inline type definition already exists
85                if doc_inline_types.contains_key(fact_ref) {
86                    return Err(LemmaError::engine(
87                        format!(
88                            "Inline type definition for fact '{}' is already defined in document '{}'",
89                            fact_ref.fact, doc
90                        ),
91                        Some(def_loc.clone()),
92                        None::<String>,
93                    ));
94                }
95
96                // Store the inline type definition
97                doc_inline_types.insert(fact_ref.clone(), def);
98            }
99        }
100        Ok(())
101    }
102
103    /// Resolve all types for a certain document
104    ///
105    /// Returns fully resolved types for the document, including named types, inline type definitions,
106    /// and a unit index. After resolution, all imports are inlined - documents are independent.
107    /// Follows `parent` chains, accumulates constraints into `specifications`.
108    /// Handles cycle detection and cross-document references.
109    ///
110    /// # Errors
111    /// Returns an error if a unit appears in multiple types within the same document (ambiguous unit).
112    pub fn resolve_types(&self, doc: &str) -> Result<ResolvedDocumentTypes, LemmaError> {
113        self.resolve_types_internal(doc, true)
114    }
115
116    /// Resolve only named types (for validation before inline type definitions are registered)
117    pub fn resolve_named_types(&self, doc: &str) -> Result<ResolvedDocumentTypes, LemmaError> {
118        self.resolve_types_internal(doc, false)
119    }
120
121    /// Resolve only inline type definitions and merge them into an existing
122    /// `ResolvedDocumentTypes` that already contains the named types.
123    ///
124    /// This avoids re-resolving named types that were already handled by
125    /// [`resolve_named_types`](Self::resolve_named_types) during the
126    /// `prepare_types` phase, preventing duplicate errors.
127    pub fn resolve_inline_types(
128        &self,
129        doc: &str,
130        mut existing: ResolvedDocumentTypes,
131    ) -> Result<ResolvedDocumentTypes, LemmaError> {
132        let mut errors = Vec::new();
133
134        // Resolve inline type definitions only
135        if let Some(doc_inline_types) = self.inline_type_definitions.get(doc) {
136            for (fact_ref, type_def) in doc_inline_types {
137                let mut visited = HashSet::new();
138                match self.resolve_inline_type_definition(doc, type_def, &mut visited)? {
139                    Some(resolved_type) => {
140                        existing
141                            .inline_type_definitions
142                            .insert(fact_ref.clone(), resolved_type);
143                    }
144                    None => {
145                        unreachable!(
146                            "BUG: registered inline type definition for fact '{}' could not be resolved (doc='{}')",
147                            fact_ref, doc
148                        );
149                    }
150                }
151            }
152        }
153
154        // Extend the unit index with units from inline type definitions
155        for (fact_ref, resolved_type) in &existing.inline_type_definitions {
156            let inline_type_name = format!("{}::{}", doc, fact_ref);
157            let e = if resolved_type.is_scale() {
158                self.add_scale_units_to_index(
159                    &mut existing.unit_index,
160                    resolved_type,
161                    doc,
162                    &inline_type_name,
163                )
164            } else if resolved_type.is_ratio() {
165                self.add_ratio_units_to_index(
166                    &mut existing.unit_index,
167                    resolved_type,
168                    doc,
169                    &inline_type_name,
170                )
171            } else {
172                Ok(())
173            };
174            if let Err(e) = e {
175                errors.push(e);
176            }
177        }
178
179        if !errors.is_empty() {
180            return Err(LemmaError::MultipleErrors(errors));
181        }
182
183        Ok(existing)
184    }
185
186    fn resolve_types_internal(
187        &self,
188        doc: &str,
189        include_anonymous: bool,
190    ) -> Result<ResolvedDocumentTypes, LemmaError> {
191        let mut named_types = HashMap::new();
192        let mut inline_type_definitions = HashMap::new();
193        let mut visited = HashSet::new();
194
195        // Resolve named types
196        if let Some(doc_types) = self.named_types.get(doc) {
197            for type_name in doc_types.keys() {
198                match self.resolve_type_internal(doc, type_name, &mut visited)? {
199                    Some(resolved_type) => {
200                        named_types.insert(type_name.clone(), resolved_type);
201                    }
202                    None => {
203                        unreachable!(
204                            "BUG: registered named type '{}' could not be resolved (doc='{}')",
205                            type_name, doc
206                        );
207                    }
208                }
209                visited.clear();
210            }
211        }
212
213        // Resolve inline type definitions (only if requested)
214        if include_anonymous {
215            if let Some(doc_inline_types) = self.inline_type_definitions.get(doc) {
216                for (fact_ref, type_def) in doc_inline_types {
217                    let mut visited = HashSet::new();
218                    match self.resolve_inline_type_definition(doc, type_def, &mut visited)? {
219                        Some(resolved_type) => {
220                            inline_type_definitions.insert(fact_ref.clone(), resolved_type);
221                        }
222                        None => {
223                            unreachable!(
224                                "BUG: registered inline type definition for fact '{}' could not be resolved (doc='{}')",
225                                fact_ref, doc
226                            );
227                        }
228                    }
229                }
230            }
231        }
232
233        // Build unit index from types that have units (primitive types first, then document types)
234        let mut unit_index: HashMap<String, LemmaType> = HashMap::new();
235        let mut errors = Vec::new();
236
237        // Add all standard ratio units to the index
238        if let Err(error) = self.add_ratio_units_to_index(
239            &mut unit_index,
240            semantics::primitive_ratio(),
241            doc,
242            "ratio",
243        ) {
244            errors.push(error);
245        }
246
247        // Add units from named types (collect all errors)
248        for resolved_type in named_types.values() {
249            let type_name = resolved_type.name.as_deref().unwrap_or("inline");
250            let e = if resolved_type.is_scale() {
251                self.add_scale_units_to_index(&mut unit_index, resolved_type, doc, type_name)
252            } else if resolved_type.is_ratio() {
253                self.add_ratio_units_to_index(&mut unit_index, resolved_type, doc, type_name)
254            } else {
255                Ok(())
256            };
257            if let Err(e) = e {
258                errors.push(e);
259            }
260        }
261
262        // Add units from inline type definitions (collect all errors)
263        for (fact_ref, resolved_type) in &inline_type_definitions {
264            let inline_type_name = format!("{}::{}", doc, fact_ref);
265            let e = if resolved_type.is_scale() {
266                self.add_scale_units_to_index(
267                    &mut unit_index,
268                    resolved_type,
269                    doc,
270                    &inline_type_name,
271                )
272            } else if resolved_type.is_ratio() {
273                self.add_ratio_units_to_index(
274                    &mut unit_index,
275                    resolved_type,
276                    doc,
277                    &inline_type_name,
278                )
279            } else {
280                Ok(())
281            };
282            if let Err(e) = e {
283                errors.push(e);
284            }
285        }
286
287        // Return all collected errors if any, each with its own real source location
288        if !errors.is_empty() {
289            return Err(LemmaError::MultipleErrors(errors));
290        }
291
292        Ok(ResolvedDocumentTypes {
293            named_types,
294            inline_type_definitions,
295            unit_index,
296        })
297    }
298
299    /// Resolve a single type with cycle detection
300    fn resolve_type_internal(
301        &self,
302        doc: &str,
303        name: &str,
304        visited: &mut HashSet<String>,
305    ) -> Result<Option<LemmaType>, LemmaError> {
306        // Cycle detection using doc::name key
307        let key = format!("{}::{}", doc, name);
308        if visited.contains(&key) {
309            let source_location = self
310                .named_types
311                .get(doc)
312                .and_then(|dt| dt.get(name))
313                .map(|td| td.source_location().clone())
314                .unwrap_or_else(|| {
315                    unreachable!(
316                        "BUG: circular dependency detected for type '{}::{}' but type definition not found in registry",
317                        doc, name
318                    )
319                });
320            return Err(LemmaError::circular_dependency(
321                format!("Circular dependency detected in type resolution: {}", key),
322                Some(source_location),
323                vec![],
324                None::<String>,
325            ));
326        }
327        visited.insert(key.clone());
328
329        // Get the TypeDef from the document (check named types)
330        let type_def = match self.named_types.get(doc).and_then(|dt| dt.get(name)) {
331            Some(def) => def.clone(),
332            None => {
333                visited.remove(&key);
334                return Ok(None);
335            }
336        };
337
338        // Resolve the parent type (standard or custom)
339        let (parent, from, constraints, type_name) = match &type_def {
340            TypeDef::Regular {
341                name,
342                parent,
343                constraints,
344                ..
345            } => (parent.clone(), None, constraints.clone(), name.clone()),
346            TypeDef::Import {
347                name,
348                source_type,
349                from,
350                constraints,
351                ..
352            } => (
353                source_type.clone(),
354                Some(from.clone()),
355                constraints.clone(),
356                name.clone(),
357            ),
358            TypeDef::Inline { .. } => {
359                // Inline types are resolved separately
360                visited.remove(&key);
361                return Ok(None);
362            }
363        };
364
365        let parent_specs = match self.resolve_parent(
366            doc,
367            &parent,
368            &from,
369            visited,
370            type_def.source_location(),
371        ) {
372            Ok(Some(specs)) => specs,
373            Ok(None) => {
374                // Parent type not found - this is an error for named types
375                // (inline type definitions might have forward references, but named types should be resolvable)
376                visited.remove(&key);
377                let source = type_def.source_location().clone();
378                return Err(LemmaError::engine(
379                    format!("Unknown type: '{}'. Type must be defined before use. Valid primitive types are: boolean, scale, number, ratio, text, date, time, duration, percent", parent),
380                    Some(source.clone()),
381                    None::<String>,
382                ));
383            }
384            Err(e) => {
385                visited.remove(&key);
386                return Err(e);
387            }
388        };
389
390        // Apply constraints from the TypeDef
391        let final_specs = if let Some(constraints) = &constraints {
392            match self.apply_constraints(parent_specs, constraints, type_def.source_location()) {
393                Ok(specs) => specs,
394                Err(errors) => {
395                    visited.remove(&key);
396                    return Err(LemmaError::MultipleErrors(errors));
397                }
398            }
399        } else {
400            parent_specs
401        };
402
403        visited.remove(&key);
404
405        // Determine extends based on whether parent is standard or custom
406        let extends = if self.resolve_primitive_type(&parent).is_some() {
407            TypeExtends::Primitive
408        } else {
409            let parent_doc = from.as_ref().map(|r| r.name.as_str()).unwrap_or(doc);
410            let family = self
411                .resolve_type_internal(parent_doc, &parent, visited)
412                .ok()
413                .flatten()
414                .and_then(|parent_type| parent_type.scale_family_name().map(String::from))
415                .unwrap_or_else(|| parent.clone());
416            TypeExtends::Custom {
417                parent: parent.clone(),
418                family,
419            }
420        };
421
422        Ok(Some(LemmaType {
423            name: Some(type_name),
424            specifications: final_specs,
425            extends,
426        }))
427    }
428
429    /// Resolve a parent type reference (standard or custom)
430    fn resolve_parent(
431        &self,
432        doc: &str,
433        parent: &str,
434        from: &Option<crate::parsing::ast::DocRef>,
435        visited: &mut HashSet<String>,
436        source: &crate::Source,
437    ) -> Result<Option<TypeSpecification>, LemmaError> {
438        // Try primitive types first
439        if let Some(specs) = self.resolve_primitive_type(parent) {
440            return Ok(Some(specs));
441        }
442
443        // Otherwise resolve as a custom type in the specified document (or same document if not specified).
444        // DocRef.name is already the clean name (@ stripped by parser).
445        let parent_doc = from.as_ref().map(|r| r.name.as_str()).unwrap_or(doc);
446        match self.resolve_type_internal(parent_doc, parent, visited) {
447            Ok(Some(t)) => Ok(Some(t.specifications)),
448            Ok(None) => {
449                // Parent type not found - check if it was ever registered
450                let type_exists = if let Some(doc_types) = self.named_types.get(parent_doc) {
451                    doc_types.contains_key(parent)
452                } else {
453                    false
454                };
455
456                if !type_exists {
457                    // Type was never registered - invalid parent type
458                    Err(LemmaError::engine(
459                        format!("Unknown type: '{}'. Type must be defined before use. Valid primitive types are: boolean, scale, number, ratio, text, date, time, duration, percent", parent),
460                        Some(source.clone()),
461                        None::<String>,
462                    ))
463                } else {
464                    // Type exists but couldn't be resolved (circular dependency or other issue)
465                    // Return None - the caller will handle this appropriately
466                    Ok(None)
467                }
468            }
469            Err(e) => Err(e),
470        }
471    }
472
473    /// Resolve a primitive type by name
474    pub fn resolve_primitive_type(&self, name: &str) -> Option<TypeSpecification> {
475        match name {
476            "boolean" => Some(TypeSpecification::boolean()),
477            "scale" => Some(TypeSpecification::scale()),
478            "number" => Some(TypeSpecification::number()),
479            "ratio" => Some(TypeSpecification::ratio()),
480            "text" => Some(TypeSpecification::text()),
481            "date" => Some(TypeSpecification::date()),
482            "time" => Some(TypeSpecification::time()),
483            "duration" => Some(TypeSpecification::duration()),
484            "percent" => Some(TypeSpecification::ratio()),
485            _ => None,
486        }
487    }
488
489    /// Apply command-argument constraints to a TypeSpecification.
490    /// Each TypeSpecification variant handles its own commands; we just apply them in order.
491    fn apply_constraints(
492        &self,
493        mut specs: TypeSpecification,
494        constraints: &[(String, Vec<CommandArg>)],
495        source: &crate::Source,
496    ) -> Result<TypeSpecification, Vec<LemmaError>> {
497        let mut errors = Vec::new();
498        for (command, args) in constraints {
499            let specs_clone = specs.clone();
500            match specs.apply_constraint(command, args) {
501                Ok(updated_specs) => specs = updated_specs,
502                Err(e) => {
503                    errors.push(LemmaError::engine(
504                        format!("Failed to apply constraint '{}': {}", command, e),
505                        Some(source.clone()),
506                        None::<String>,
507                    ));
508                    specs = specs_clone;
509                }
510            }
511        }
512        if !errors.is_empty() {
513            return Err(errors);
514        }
515        Ok(specs)
516    }
517
518    /// Resolve an inline type definition from its definition
519    fn resolve_inline_type_definition(
520        &self,
521        doc: &str,
522        type_def: &TypeDef,
523        visited: &mut HashSet<String>,
524    ) -> Result<Option<LemmaType>, LemmaError> {
525        let def_loc = type_def.source_location().clone();
526        let TypeDef::Inline {
527            parent,
528            constraints,
529            fact_ref: _,
530            from,
531            ..
532        } = type_def
533        else {
534            return Ok(None);
535        };
536
537        let parent_specs = match self.resolve_parent(doc, parent, from, visited, &def_loc) {
538            Ok(Some(specs)) => specs,
539            Ok(None) => {
540                // Parent type not found - this is an error for inline type definitions too
541                // Inline type definitions should have valid parent types
542                return Err(LemmaError::engine(
543                    format!("Unknown type: '{}'. Type must be defined before use. Valid primitive types are: boolean, scale, number, ratio, text, date, time, duration, percent", parent),
544                    Some(def_loc.clone()),
545                    None::<String>,
546                ));
547            }
548            Err(e) => return Err(e),
549        };
550
551        let final_specs = if let Some(constraints) = constraints {
552            match self.apply_constraints(parent_specs, constraints, &def_loc) {
553                Ok(specs) => specs,
554                Err(errors) => {
555                    return Err(LemmaError::MultipleErrors(errors));
556                }
557            }
558        } else {
559            parent_specs
560        };
561
562        // Determine extends based on whether parent is standard or custom
563        let extends = if self.resolve_primitive_type(parent).is_some() {
564            TypeExtends::Primitive
565        } else {
566            let parent_doc = from.as_ref().map(|r| r.name.as_str()).unwrap_or(doc);
567            let family = self
568                .resolve_type_internal(parent_doc, parent, visited)
569                .ok()
570                .flatten()
571                .and_then(|parent_type| parent_type.scale_family_name().map(String::from))
572                .unwrap_or_else(|| parent.to_string());
573            TypeExtends::Custom {
574                parent: parent.to_string(),
575                family,
576            }
577        };
578
579        Ok(Some(LemmaType::without_name(final_specs, extends)))
580    }
581
582    /// Add units from a scale type to the unit index.
583    /// Same unit in same type = error. Same unit in scale extension chain (same family) = allow. Otherwise ambiguous.
584    fn add_scale_units_to_index(
585        &self,
586        unit_index: &mut HashMap<String, LemmaType>,
587        resolved_type: &LemmaType,
588        doc: &str,
589        type_name: &str,
590    ) -> Result<(), LemmaError> {
591        let units = self.extract_units_from_specs(&resolved_type.specifications);
592        for unit in units {
593            if let Some(existing_type) = unit_index.get(&unit) {
594                let existing_name = existing_type.name.as_deref().unwrap_or("inline");
595                let same_type = existing_type.name.as_deref() == resolved_type.name.as_deref();
596
597                if same_type {
598                    let source = self
599                        .named_types
600                        .get(doc)
601                        .and_then(|defs| defs.get(type_name))
602                        .map(|def| def.source_location().clone())
603                        .expect("BUG: named type definition must have source location");
604
605                    return Err(LemmaError::engine(
606                        format!(
607                            "Unit '{}' is defined more than once in type '{}'",
608                            unit, type_name
609                        ),
610                        Some(source.clone()),
611                        None::<String>,
612                    ));
613                }
614
615                let current_extends_existing = resolved_type
616                    .extends
617                    .parent_name()
618                    .map(|p| existing_name == p)
619                    .unwrap_or(false);
620                let existing_extends_current = existing_type
621                    .extends
622                    .parent_name()
623                    .map(|p| p == resolved_type.name.as_deref().unwrap_or(""))
624                    .unwrap_or(false);
625
626                if existing_type.is_scale()
627                    && (current_extends_existing || existing_extends_current)
628                {
629                    if current_extends_existing {
630                        unit_index.insert(unit, resolved_type.clone());
631                    }
632                    continue;
633                }
634
635                // Siblings in the same scale family (e.g. both extend "money")
636                // inherit the same unit — not ambiguous.
637                if existing_type.same_scale_family(resolved_type) {
638                    continue;
639                }
640
641                let source = self
642                    .named_types
643                    .get(doc)
644                    .and_then(|defs| defs.get(type_name))
645                    .map(|def| def.source_location().clone())
646                    .expect("BUG: named type definition must have source location");
647
648                return Err(LemmaError::engine(
649                    format!(
650                        "Ambiguous unit '{}' in document '{}'. Defined in multiple types: {} and {}",
651                        unit, doc, existing_name, type_name
652                    ),
653                    Some(source.clone()),
654                    None::<String>,
655                ));
656            }
657            unit_index.insert(unit, resolved_type.clone());
658        }
659        Ok(())
660    }
661
662    /// Add ratio units to the unit index. Ratio units are document-scoped singleton: merged across all ratio types.
663    fn add_ratio_units_to_index(
664        &self,
665        unit_index: &mut HashMap<String, LemmaType>,
666        resolved_type: &LemmaType,
667        doc: &str,
668        type_name: &str,
669    ) -> Result<(), LemmaError> {
670        let units = self.extract_units_from_specs(&resolved_type.specifications);
671        for unit in units {
672            if let Some(existing_type) = unit_index.get(&unit) {
673                if existing_type.is_ratio() {
674                    continue;
675                }
676                let existing_name = existing_type.name.as_deref().unwrap_or("inline");
677                let source = self
678                    .named_types
679                    .get(doc)
680                    .and_then(|defs| defs.get(type_name))
681                    .map(|def| def.source_location().clone())
682                    .expect("BUG: named type definition must have source location");
683
684                return Err(LemmaError::engine(
685                    format!(
686                        "Ambiguous unit '{}' in document '{}'. Defined in multiple types: {} and {}",
687                        unit, doc, existing_name, type_name
688                    ),
689                    Some(source.clone()),
690                    None::<String>,
691                ));
692            }
693            unit_index.insert(unit, resolved_type.clone());
694        }
695        Ok(())
696    }
697
698    /// Extract all unit names from a TypeSpecification
699    /// Only Scale types can have units (Number types are dimensionless)
700    fn extract_units_from_specs(&self, specs: &TypeSpecification) -> Vec<String> {
701        match specs {
702            TypeSpecification::Scale { units, .. } => {
703                units.iter().map(|unit| unit.name.clone()).collect()
704            }
705            TypeSpecification::Ratio { units, .. } => {
706                units.iter().map(|unit| unit.name.clone()).collect()
707            }
708            _ => Vec::new(),
709        }
710    }
711}
712
713impl Default for TypeRegistry {
714    fn default() -> Self {
715        Self::new(HashMap::new())
716    }
717}
718
719#[cfg(test)]
720mod tests {
721    use super::*;
722    use crate::parse;
723    use crate::ResourceLimits;
724    use rust_decimal::Decimal;
725    use std::sync::Arc;
726
727    fn test_registry() -> TypeRegistry {
728        let mut sources = HashMap::new();
729        sources.insert("<test>".to_string(), String::new());
730        sources.insert("test.lemma".to_string(), String::new());
731        TypeRegistry::new(sources)
732    }
733
734    #[test]
735    fn test_registry_creation() {
736        let registry = test_registry();
737        assert!(registry.named_types.is_empty());
738        assert!(registry.inline_type_definitions.is_empty());
739    }
740
741    #[test]
742    fn test_resolve_primitive_types() {
743        let registry = test_registry();
744
745        assert!(registry.resolve_primitive_type("boolean").is_some());
746        assert!(registry.resolve_primitive_type("scale").is_some());
747        assert!(registry.resolve_primitive_type("number").is_some());
748        assert!(registry.resolve_primitive_type("ratio").is_some());
749        assert!(registry.resolve_primitive_type("text").is_some());
750        assert!(registry.resolve_primitive_type("date").is_some());
751        assert!(registry.resolve_primitive_type("time").is_some());
752        assert!(registry.resolve_primitive_type("duration").is_some());
753        assert!(registry.resolve_primitive_type("unknown").is_none());
754    }
755
756    #[test]
757    fn test_register_named_type() {
758        let mut registry = test_registry();
759        let type_def = TypeDef::Regular {
760            source_location: crate::Source::new(
761                "<test>",
762                crate::parsing::ast::Span {
763                    start: 0,
764                    end: 0,
765                    line: 1,
766                    col: 0,
767                },
768                "test_doc",
769                Arc::from("doc test\nfact x = 1"),
770            ),
771            name: "money".to_string(),
772            parent: "number".to_string(),
773            constraints: None,
774        };
775
776        let result = registry.register_type("test_doc", type_def);
777        assert!(result.is_ok());
778    }
779
780    #[test]
781    fn test_register_inline_type_definition() {
782        use crate::parsing::ast::FactReference;
783        let mut registry = test_registry();
784        let fact_ref = FactReference::local("age".to_string());
785        let type_def = TypeDef::Inline {
786            source_location: crate::Source::new(
787                "<test>",
788                crate::parsing::ast::Span {
789                    start: 0,
790                    end: 0,
791                    line: 1,
792                    col: 0,
793                },
794                "test_doc",
795                Arc::from("doc test\nfact x = 1"),
796            ),
797            parent: "number".to_string(),
798            constraints: Some(vec![
799                (
800                    "minimum".to_string(),
801                    vec![CommandArg::Number("0".to_string())],
802                ),
803                (
804                    "maximum".to_string(),
805                    vec![CommandArg::Number("150".to_string())],
806                ),
807            ]),
808            fact_ref: fact_ref.clone(),
809            from: None,
810        };
811
812        let result = registry.register_type("test_doc", type_def);
813        assert!(result.is_ok());
814
815        // Verify the inline type definition is registered
816        assert!(registry
817            .inline_type_definitions
818            .get("test_doc")
819            .unwrap()
820            .contains_key(&fact_ref));
821    }
822
823    #[test]
824    fn test_register_duplicate_type_fails() {
825        let mut registry = test_registry();
826        let type_def = TypeDef::Regular {
827            source_location: crate::Source::new(
828                "<test>",
829                crate::parsing::ast::Span {
830                    start: 0,
831                    end: 0,
832                    line: 1,
833                    col: 0,
834                },
835                "test_doc",
836                Arc::from("doc test\nfact x = 1"),
837            ),
838            name: "money".to_string(),
839            parent: "number".to_string(),
840            constraints: None,
841        };
842
843        registry
844            .register_type("test_doc", type_def.clone())
845            .unwrap();
846        let result = registry.register_type("test_doc", type_def);
847        assert!(result.is_err());
848    }
849
850    #[test]
851    fn test_resolve_custom_type_from_primitive() {
852        let mut registry = test_registry();
853        let type_def = TypeDef::Regular {
854            source_location: crate::Source::new(
855                "<test>",
856                crate::parsing::ast::Span {
857                    start: 0,
858                    end: 0,
859                    line: 1,
860                    col: 0,
861                },
862                "test_doc",
863                Arc::from("doc test\nfact x = 1"),
864            ),
865            name: "money".to_string(),
866            parent: "number".to_string(),
867            constraints: None,
868        };
869
870        registry.register_type("test_doc", type_def).unwrap();
871        let resolved = registry.resolve_types("test_doc").unwrap();
872
873        assert!(resolved.named_types.contains_key("money"));
874        let money_type = resolved.named_types.get("money").unwrap();
875        assert_eq!(money_type.name, Some("money".to_string()));
876    }
877
878    #[test]
879    fn test_type_definition_resolution() {
880        let code = r#"doc test
881type dice = number -> minimum 0 -> maximum 6"#;
882
883        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
884        let doc = &docs[0];
885
886        // Use TypeRegistry to resolve the type
887        let mut registry = test_registry();
888        registry
889            .register_type(&doc.name, doc.types[0].clone())
890            .unwrap();
891
892        let resolved_types = registry.resolve_types(&doc.name).unwrap();
893        let dice_type = resolved_types.named_types.get("dice").unwrap();
894
895        // Verify it's a Number type (dimensionless) with the correct constraints
896        match &dice_type.specifications {
897            TypeSpecification::Number {
898                minimum, maximum, ..
899            } => {
900                assert_eq!(*minimum, Some(Decimal::from(0)));
901                assert_eq!(*maximum, Some(Decimal::from(6)));
902            }
903            _ => panic!("Expected Number type specifications"),
904        }
905    }
906
907    #[test]
908    fn test_type_definition_with_multiple_commands() {
909        let code = r#"doc test
910type money = scale -> decimals 2 -> unit eur 1.0 -> unit usd 1.18"#;
911
912        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
913        let doc = &docs[0];
914        let type_def = &doc.types[0];
915
916        // Use TypeRegistry to resolve the type
917        let mut registry = test_registry();
918        registry.register_type(&doc.name, type_def.clone()).unwrap();
919
920        let resolved_types = registry.resolve_types(&doc.name).unwrap();
921        let money_type = resolved_types.named_types.get("money").unwrap();
922
923        match &money_type.specifications {
924            TypeSpecification::Scale {
925                decimals, units, ..
926            } => {
927                assert_eq!(*decimals, Some(2));
928                assert_eq!(units.len(), 2);
929                assert!(units.iter().any(|u| u.name == "eur"));
930                assert!(units.iter().any(|u| u.name == "usd"));
931            }
932            _ => panic!("Expected Scale type specifications"),
933        }
934    }
935
936    #[test]
937    fn test_number_type_with_decimals() {
938        let code = r#"doc test
939type price = number -> decimals 2 -> minimum 0"#;
940
941        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
942        let doc = &docs[0];
943
944        // Use TypeRegistry to resolve the type
945        let mut registry = test_registry();
946        registry
947            .register_type(&doc.name, doc.types[0].clone())
948            .unwrap();
949
950        let resolved_types = registry.resolve_types(&doc.name).unwrap();
951        let price_type = resolved_types.named_types.get("price").unwrap();
952
953        // Verify it's a Number type with decimals set to 2
954        match &price_type.specifications {
955            TypeSpecification::Number {
956                decimals, minimum, ..
957            } => {
958                assert_eq!(*decimals, Some(2));
959                assert_eq!(*minimum, Some(Decimal::from(0)));
960            }
961            _ => panic!("Expected Number type specifications with decimals"),
962        }
963    }
964
965    #[test]
966    fn test_number_type_decimals_only() {
967        let code = r#"doc test
968type precise_number = number -> decimals 4"#;
969
970        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
971        let doc = &docs[0];
972
973        let mut registry = test_registry();
974        registry
975            .register_type(&doc.name, doc.types[0].clone())
976            .unwrap();
977
978        let resolved_types = registry.resolve_types(&doc.name).unwrap();
979        let precise_type = resolved_types.named_types.get("precise_number").unwrap();
980
981        match &precise_type.specifications {
982            TypeSpecification::Number { decimals, .. } => {
983                assert_eq!(*decimals, Some(4));
984            }
985            _ => panic!("Expected Number type with decimals 4"),
986        }
987    }
988
989    #[test]
990    fn test_scale_type_decimals_only() {
991        let code = r#"doc test
992type weight = scale -> unit kg 1 -> decimals 3"#;
993
994        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
995        let doc = &docs[0];
996
997        let mut registry = test_registry();
998        registry
999            .register_type(&doc.name, doc.types[0].clone())
1000            .unwrap();
1001
1002        let resolved_types = registry.resolve_types(&doc.name).unwrap();
1003        let weight_type = resolved_types.named_types.get("weight").unwrap();
1004
1005        match &weight_type.specifications {
1006            TypeSpecification::Scale { decimals, .. } => {
1007                assert_eq!(*decimals, Some(3));
1008            }
1009            _ => panic!("Expected Scale type with decimals 3"),
1010        }
1011    }
1012
1013    #[test]
1014    fn test_ratio_type_accepts_optional_decimals_command() {
1015        let code = r#"doc test
1016type ratio_type = ratio -> decimals 2"#;
1017
1018        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
1019        let doc = &docs[0];
1020
1021        let mut registry = test_registry();
1022        registry
1023            .register_type(&doc.name, doc.types[0].clone())
1024            .unwrap();
1025
1026        let resolved_types = registry.resolve_types(&doc.name).unwrap();
1027        let ratio_type = resolved_types.named_types.get("ratio_type").unwrap();
1028
1029        match &ratio_type.specifications {
1030            TypeSpecification::Ratio { decimals, .. } => {
1031                assert_eq!(
1032                    *decimals,
1033                    Some(2),
1034                    "ratio type should accept decimals command"
1035                );
1036            }
1037            _ => panic!("Expected Ratio type with decimals 2"),
1038        }
1039    }
1040
1041    #[test]
1042    fn test_ratio_type_with_default_command() {
1043        let code = r#"doc test
1044type percentage = ratio -> minimum 0 -> maximum 1 -> default 0.5"#;
1045
1046        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
1047        let doc = &docs[0];
1048
1049        let mut registry = test_registry();
1050        registry
1051            .register_type(&doc.name, doc.types[0].clone())
1052            .unwrap();
1053
1054        let resolved_types = registry.resolve_types(&doc.name).unwrap();
1055        let percentage_type = resolved_types.named_types.get("percentage").unwrap();
1056
1057        match &percentage_type.specifications {
1058            TypeSpecification::Ratio {
1059                minimum,
1060                maximum,
1061                default,
1062                ..
1063            } => {
1064                assert_eq!(
1065                    *minimum,
1066                    Some(Decimal::from(0)),
1067                    "ratio type should have minimum 0"
1068                );
1069                assert_eq!(
1070                    *maximum,
1071                    Some(Decimal::from(1)),
1072                    "ratio type should have maximum 1"
1073                );
1074                assert_eq!(
1075                    *default,
1076                    Some(Decimal::from_i128_with_scale(5, 1)),
1077                    "ratio type with default command must work"
1078                );
1079            }
1080            _ => panic!("Expected Ratio type with minimum, maximum, and default"),
1081        }
1082    }
1083
1084    #[test]
1085    fn test_scale_extension_chain_same_family_units_allowed() {
1086        let code = r#"doc test
1087type money = scale -> unit eur 1
1088type money2 = money -> unit usd 1.24"#;
1089
1090        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
1091        let doc = &docs[0];
1092
1093        let mut registry = test_registry();
1094        for type_def in &doc.types {
1095            registry.register_type(&doc.name, type_def.clone()).unwrap();
1096        }
1097
1098        let result = registry.resolve_types(&doc.name);
1099        assert!(
1100            result.is_ok(),
1101            "Scale extension chain should resolve: {:?}",
1102            result.err()
1103        );
1104
1105        let resolved = result.unwrap();
1106        assert!(
1107            resolved.unit_index.contains_key("eur"),
1108            "eur should be in unit_index"
1109        );
1110        assert!(
1111            resolved.unit_index.contains_key("usd"),
1112            "usd should be in unit_index"
1113        );
1114        let eur_type = resolved.unit_index.get("eur").unwrap();
1115        let usd_type = resolved.unit_index.get("usd").unwrap();
1116        assert_eq!(
1117            eur_type.name.as_deref(),
1118            Some("money2"),
1119            "more derived type (money2) should own eur for conversion"
1120        );
1121        assert_eq!(usd_type.name.as_deref(), Some("money2"));
1122    }
1123
1124    #[test]
1125    fn test_invalid_parent_type_in_named_type_should_error() {
1126        let code = r#"doc test
1127type invalid = nonexistent_type -> minimum 0"#;
1128
1129        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
1130        let doc = &docs[0];
1131
1132        let mut registry = test_registry();
1133        registry
1134            .register_type(&doc.name, doc.types[0].clone())
1135            .unwrap();
1136
1137        let result = registry.resolve_types(&doc.name);
1138        assert!(result.is_err(), "Should reject invalid parent type");
1139
1140        let error_msg = result.unwrap_err().to_string();
1141        assert!(
1142            error_msg.contains("Unknown type") && error_msg.contains("nonexistent_type"),
1143            "Error should mention unknown type. Got: {}",
1144            error_msg
1145        );
1146    }
1147
1148    #[test]
1149    fn test_invalid_primitive_type_name_should_error() {
1150        // "choice" is not a primitive type; this should fail resolution.
1151        let code = r#"doc test
1152type invalid = choice -> option "a""#;
1153
1154        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
1155        let doc = &docs[0];
1156
1157        let mut registry = test_registry();
1158        registry
1159            .register_type(&doc.name, doc.types[0].clone())
1160            .unwrap();
1161
1162        let result = registry.resolve_types(&doc.name);
1163        assert!(result.is_err(), "Should reject invalid type base 'choice'");
1164
1165        let error_msg = result.unwrap_err().to_string();
1166        assert!(
1167            error_msg.contains("Unknown type") && error_msg.contains("choice"),
1168            "Error should mention unknown type 'choice'. Got: {}",
1169            error_msg
1170        );
1171    }
1172
1173    #[test]
1174    fn test_unit_constraint_validation_errors_are_reported() {
1175        // Regression guard: overriding existing units should not silently succeed.
1176        let code = r#"doc test
1177type money = scale
1178  -> unit eur 1.00
1179  -> unit usd 1.19
1180
1181type money2 = money
1182  -> unit eur 1.20
1183  -> unit usd 1.21
1184  -> unit gbp 1.30"#;
1185
1186        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
1187        let doc = &docs[0];
1188
1189        let mut registry = test_registry();
1190        for type_def in &doc.types {
1191            registry.register_type(&doc.name, type_def.clone()).unwrap();
1192        }
1193
1194        let result = registry.resolve_types(&doc.name);
1195        assert!(
1196            result.is_err(),
1197            "Expected unit constraint conflicts to error"
1198        );
1199
1200        let error_msg = result.unwrap_err().to_string();
1201        assert!(
1202            error_msg.contains("eur") || error_msg.contains("usd"),
1203            "Error should mention the conflicting units. Got: {}",
1204            error_msg
1205        );
1206    }
1207
1208    #[test]
1209    fn test_document_level_unit_ambiguity_errors_are_reported() {
1210        // Regression guard: the same unit name must not be defined by multiple types in one doc.
1211        let code = r#"doc test
1212type money_a = scale
1213  -> unit eur 1.00
1214  -> unit usd 1.19
1215
1216type money_b = scale
1217  -> unit eur 1.00
1218  -> unit usd 1.20
1219
1220type length_a = scale
1221  -> unit meter 1.0
1222
1223type length_b = scale
1224  -> unit meter 1.0"#;
1225
1226        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
1227        let doc = &docs[0];
1228
1229        let mut registry = test_registry();
1230        for type_def in &doc.types {
1231            registry.register_type(&doc.name, type_def.clone()).unwrap();
1232        }
1233
1234        let result = registry.resolve_types(&doc.name);
1235        assert!(
1236            result.is_err(),
1237            "Expected ambiguous unit definitions to error"
1238        );
1239
1240        let error_msg = result.unwrap_err().to_string();
1241        assert!(
1242            error_msg.contains("eur") || error_msg.contains("usd") || error_msg.contains("meter"),
1243            "Error should mention at least one ambiguous unit. Got: {}",
1244            error_msg
1245        );
1246    }
1247
1248    #[test]
1249    fn test_number_type_cannot_have_units() {
1250        let code = r#"doc test
1251type price = number
1252  -> unit eur 1.00"#;
1253
1254        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
1255        let doc = &docs[0];
1256
1257        let mut registry = test_registry();
1258        registry
1259            .register_type(&doc.name, doc.types[0].clone())
1260            .unwrap();
1261
1262        let result = registry.resolve_types(&doc.name);
1263        assert!(result.is_err(), "Number types must reject unit commands");
1264
1265        let error_msg = result.unwrap_err().to_string();
1266        assert!(
1267            error_msg.contains("unit") && error_msg.contains("number"),
1268            "Error should mention units are invalid on number. Got: {}",
1269            error_msg
1270        );
1271    }
1272
1273    #[test]
1274    fn test_scale_type_can_have_units() {
1275        let code = r#"doc test
1276type money = scale
1277  -> unit eur 1.00
1278  -> unit usd 1.19"#;
1279
1280        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
1281        let doc = &docs[0];
1282
1283        let mut registry = test_registry();
1284        registry
1285            .register_type(&doc.name, doc.types[0].clone())
1286            .unwrap();
1287
1288        let resolved = registry.resolve_types(&doc.name).unwrap();
1289        let money_type = resolved.named_types.get("money").unwrap();
1290
1291        match &money_type.specifications {
1292            TypeSpecification::Scale { units, .. } => {
1293                assert_eq!(units.len(), 2);
1294                assert!(units.iter().any(|u| u.name == "eur"));
1295                assert!(units.iter().any(|u| u.name == "usd"));
1296            }
1297            other => panic!("Expected Scale type specifications, got {:?}", other),
1298        }
1299    }
1300
1301    #[test]
1302    fn test_extending_type_inherits_units() {
1303        let code = r#"doc test
1304type money = scale
1305  -> unit eur 1.00
1306  -> unit usd 1.19
1307
1308type my_money = money
1309  -> unit gbp 1.30"#;
1310
1311        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
1312        let doc = &docs[0];
1313
1314        let mut registry = test_registry();
1315        for type_def in &doc.types {
1316            registry.register_type(&doc.name, type_def.clone()).unwrap();
1317        }
1318
1319        let resolved = registry.resolve_types(&doc.name).unwrap();
1320        let my_money_type = resolved.named_types.get("my_money").unwrap();
1321
1322        match &my_money_type.specifications {
1323            TypeSpecification::Scale { units, .. } => {
1324                assert_eq!(units.len(), 3);
1325                assert!(units.iter().any(|u| u.name == "eur"));
1326                assert!(units.iter().any(|u| u.name == "usd"));
1327                assert!(units.iter().any(|u| u.name == "gbp"));
1328            }
1329            other => panic!("Expected Scale type specifications, got {:?}", other),
1330        }
1331    }
1332
1333    #[test]
1334    fn test_duplicate_unit_in_same_type_is_rejected() {
1335        let code = r#"doc test
1336type money = scale
1337  -> unit eur 1.00
1338  -> unit eur 1.19"#;
1339
1340        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
1341        let doc = &docs[0];
1342
1343        let mut registry = test_registry();
1344        registry
1345            .register_type(&doc.name, doc.types[0].clone())
1346            .unwrap();
1347
1348        let result = registry.resolve_types(&doc.name);
1349        assert!(
1350            result.is_err(),
1351            "Duplicate units within a type should error"
1352        );
1353
1354        let error_msg = result.unwrap_err().to_string();
1355        assert!(
1356            error_msg.contains("Duplicate unit")
1357                || error_msg.contains("duplicate")
1358                || error_msg.contains("already exists")
1359                || error_msg.contains("eur"),
1360            "Error should mention duplicate unit issue. Got: {}",
1361            error_msg
1362        );
1363    }
1364}