1use 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#[derive(Debug, Clone)]
18pub struct ResolvedDocumentTypes {
19 pub named_types: HashMap<String, LemmaType>,
21
22 pub inline_type_definitions: HashMap<FactReference, LemmaType>,
24
25 pub unit_index: HashMap<String, LemmaType>,
28}
29
30#[derive(Debug, Clone)]
35pub struct TypeRegistry {
36 named_types: HashMap<String, HashMap<String, TypeDef>>,
39 inline_type_definitions: HashMap<String, HashMap<FactReference, TypeDef>>,
42 _sources: HashMap<String, String>,
45}
46
47impl TypeRegistry {
48 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 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 let doc_types = self.named_types.entry(doc.to_string()).or_default();
64
65 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 doc_types.insert(name.clone(), def);
76 }
77 TypeDef::Inline { fact_ref, .. } => {
78 let doc_inline_types = self
80 .inline_type_definitions
81 .entry(doc.to_string())
82 .or_default();
83
84 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 doc_inline_types.insert(fact_ref.clone(), def);
98 }
99 }
100 Ok(())
101 }
102
103 pub fn resolve_types(&self, doc: &str) -> Result<ResolvedDocumentTypes, LemmaError> {
113 self.resolve_types_internal(doc, true)
114 }
115
116 pub fn resolve_named_types(&self, doc: &str) -> Result<ResolvedDocumentTypes, LemmaError> {
118 self.resolve_types_internal(doc, false)
119 }
120
121 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 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 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 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 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 let mut unit_index: HashMap<String, LemmaType> = HashMap::new();
235 let mut errors = Vec::new();
236
237 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 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 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 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 fn resolve_type_internal(
301 &self,
302 doc: &str,
303 name: &str,
304 visited: &mut HashSet<String>,
305 ) -> Result<Option<LemmaType>, LemmaError> {
306 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 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 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 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 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 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 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 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 if let Some(specs) = self.resolve_primitive_type(parent) {
440 return Ok(Some(specs));
441 }
442
443 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 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 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 Ok(None)
467 }
468 }
469 Err(e) => Err(e),
470 }
471 }
472
473 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}