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