1use crate::engine::Context;
13use crate::error::Error;
14use crate::parsing::ast::FactValue as ParsedFactValue;
15use crate::parsing::ast::{
16 self as ast, Constraint, DateTimeValue, LemmaSpec, ParentType, Reference, TypeDef,
17};
18use crate::planning::semantics::{
19 self, LemmaType, TypeDefiningSpec, TypeExtends, TypeSpecification,
20};
21use crate::planning::validation::validate_type_specifications;
22
23use std::collections::{HashMap, HashSet};
24use std::sync::Arc;
25
26#[derive(Debug, Clone)]
29pub struct ResolvedSpecTypes {
30 pub named_types: HashMap<String, LemmaType>,
32
33 pub inline_type_definitions: HashMap<Reference, LemmaType>,
35
36 pub unit_index: HashMap<String, (LemmaType, Option<TypeDef>)>,
41}
42
43#[derive(Debug, Clone)]
45pub(crate) struct ResolvedParentSpec {
46 pub spec: Arc<LemmaSpec>,
47 pub resolved_plan_hash: Option<String>,
49}
50
51#[derive(Debug, Clone)]
58pub(crate) struct PerSliceTypeResolver<'a> {
59 named_types: HashMap<Arc<LemmaSpec>, HashMap<String, TypeDef>>,
60 inline_type_definitions: HashMap<Arc<LemmaSpec>, HashMap<Reference, TypeDef>>,
61 context: &'a Context,
62 resolve_at: Option<DateTimeValue>,
63 plan_hashes: &'a super::PlanHashRegistry,
64 all_registered_specs: Vec<Arc<LemmaSpec>>,
67}
68
69impl<'a> PerSliceTypeResolver<'a> {
70 pub fn new(
71 context: &'a Context,
72 resolve_at: Option<DateTimeValue>,
73 plan_hashes: &'a super::PlanHashRegistry,
74 ) -> Self {
75 PerSliceTypeResolver {
76 named_types: HashMap::new(),
77 inline_type_definitions: HashMap::new(),
78 context,
79 resolve_at,
80 plan_hashes,
81 all_registered_specs: Vec::new(),
82 }
83 }
84
85 pub fn register_all(&mut self, spec: &Arc<LemmaSpec>) -> Vec<Error> {
87 if !self
88 .all_registered_specs
89 .iter()
90 .any(|s| Arc::ptr_eq(s, spec))
91 {
92 self.all_registered_specs.push(Arc::clone(spec));
93 }
94
95 let mut errors = Vec::new();
96 for type_def in &spec.types {
97 let type_name = match type_def {
98 ast::TypeDef::Regular { name, .. } | ast::TypeDef::Import { name, .. } => {
99 Some(name.as_str())
100 }
101 ast::TypeDef::Inline { .. } => None,
102 };
103 if let Some(name) = type_name {
104 if let Err(e) = crate::limits::check_max_length(
105 name,
106 crate::limits::MAX_TYPE_NAME_LENGTH,
107 "type",
108 Some(type_def.source_location().clone()),
109 ) {
110 errors.push(e);
111 continue;
112 }
113 }
114 if let Err(e) = self.register_type(spec, type_def.clone()) {
115 errors.push(e);
116 }
117 }
118 errors
119 }
120
121 pub fn register_type(&mut self, spec: &Arc<LemmaSpec>, def: TypeDef) -> Result<(), Error> {
123 if !self
124 .all_registered_specs
125 .iter()
126 .any(|s| Arc::ptr_eq(s, spec))
127 {
128 self.all_registered_specs.push(Arc::clone(spec));
129 }
130
131 let def_loc = def.source_location().clone();
132 let spec_name = &spec.name;
133 match &def {
134 TypeDef::Regular { name, .. } | TypeDef::Import { name, .. } => {
135 let spec_types = self.named_types.entry(Arc::clone(spec)).or_default();
136 if spec_types.contains_key(name) {
137 return Err(Error::validation_with_context(
138 format!("Type '{}' is already defined in spec '{}'", name, spec_name),
139 Some(def_loc.clone()),
140 None::<String>,
141 Some(Arc::clone(spec)),
142 None,
143 ));
144 }
145 spec_types.insert(name.clone(), def);
146 }
147 TypeDef::Inline { fact_ref, .. } => {
148 let spec_inline_types = self
149 .inline_type_definitions
150 .entry(Arc::clone(spec))
151 .or_default();
152 if spec_inline_types.contains_key(fact_ref) {
153 return Err(Error::validation_with_context(
154 format!(
155 "Inline type definition for fact '{}' is already defined in spec '{}'",
156 fact_ref.name, spec_name
157 ),
158 Some(def_loc.clone()),
159 None::<String>,
160 Some(Arc::clone(spec)),
161 None,
162 ));
163 }
164 spec_inline_types.insert(fact_ref.clone(), def);
165 }
166 }
167 Ok(())
168 }
169
170 pub fn register_dependency_specs(&mut self, spec: &Arc<LemmaSpec>) -> Vec<Error> {
179 let mut errors = Vec::new();
180 let mut visited_spec_names: HashSet<String> = HashSet::new();
181 visited_spec_names.insert(spec.name.clone());
182 self.register_dependency_specs_recursive(spec, &mut visited_spec_names, &mut errors);
183 errors
184 }
185
186 fn register_dependency_specs_recursive(
187 &mut self,
188 spec: &Arc<LemmaSpec>,
189 visited: &mut HashSet<String>,
190 errors: &mut Vec<Error>,
191 ) {
192 for type_def in &spec.types {
193 if let TypeDef::Import { from, .. } = type_def {
194 self.try_register_spec(&from.name, from.effective.as_ref(), visited, errors);
195 }
196 }
197
198 for fact in &spec.facts {
199 match &fact.value {
200 ParsedFactValue::SpecReference(spec_ref) => {
201 self.try_register_spec(
202 &spec_ref.name,
203 spec_ref.effective.as_ref(),
204 visited,
205 errors,
206 );
207 }
208 ParsedFactValue::TypeDeclaration {
209 from: Some(from_ref),
210 ..
211 } => {
212 self.try_register_spec(
213 &from_ref.name,
214 from_ref.effective.as_ref(),
215 visited,
216 errors,
217 );
218 }
219 _ => {}
220 }
221 }
222 }
223
224 fn try_register_spec(
225 &mut self,
226 name: &str,
227 explicit_effective: Option<&DateTimeValue>,
228 visited: &mut HashSet<String>,
229 errors: &mut Vec<Error>,
230 ) {
231 if visited.contains(name) {
232 return;
233 }
234 visited.insert(name.to_string());
235
236 let at = explicit_effective.or(self.resolve_at.as_ref());
237 let dep_spec = match at {
238 Some(dt) => self.context.get_spec(name, dt),
239 None => self.context.specs_for_name(name).into_iter().next(),
240 };
241
242 if let Some(dep_spec) = dep_spec {
243 errors.extend(self.register_all(&dep_spec));
244 self.register_dependency_specs_recursive(&dep_spec, visited, errors);
245 }
246 }
247
248 pub fn resolve_all_registered_specs(
251 &self,
252 ) -> (HashMap<Arc<LemmaSpec>, ResolvedSpecTypes>, Vec<Error>) {
253 let mut result = HashMap::new();
254 let mut errors = Vec::new();
255
256 for spec_arc in &self.all_registered_specs {
257 match self.resolve_and_validate_named_types(spec_arc) {
258 Ok(resolved_types) => {
259 result.insert(Arc::clone(spec_arc), resolved_types);
260 }
261 Err(es) => errors.extend(es),
262 }
263 }
264
265 (result, errors)
266 }
267
268 pub fn resolve_and_validate_named_types(
270 &self,
271 spec: &Arc<LemmaSpec>,
272 ) -> Result<ResolvedSpecTypes, Vec<Error>> {
273 let resolved_types = self.resolve_named_types(spec)?;
274 let mut errors = Vec::new();
275
276 for (type_name, lemma_type) in &resolved_types.named_types {
277 let source = spec
278 .types
279 .iter()
280 .find(|td| match td {
281 ast::TypeDef::Regular { name, .. } | ast::TypeDef::Import { name, .. } => {
282 name == type_name
283 }
284 ast::TypeDef::Inline { .. } => false,
285 })
286 .map(|td| td.source_location().clone())
287 .unwrap_or_else(|| {
288 unreachable!(
289 "BUG: resolved named type '{}' has no corresponding TypeDef in spec '{}'",
290 type_name, spec.name
291 )
292 });
293 let mut spec_errors = validate_type_specifications(
294 &lemma_type.specifications,
295 type_name,
296 &source,
297 Some(Arc::clone(spec)),
298 );
299 errors.append(&mut spec_errors);
300 }
301
302 if errors.is_empty() {
303 Ok(resolved_types)
304 } else {
305 Err(errors)
306 }
307 }
308
309 pub fn resolve_named_types(
311 &self,
312 spec: &Arc<LemmaSpec>,
313 ) -> Result<ResolvedSpecTypes, Vec<Error>> {
314 self.resolve_types_internal(spec, false)
315 }
316
317 pub fn resolve_inline_types(
320 &self,
321 spec: &Arc<LemmaSpec>,
322 mut existing: ResolvedSpecTypes,
323 ) -> Result<ResolvedSpecTypes, Vec<Error>> {
324 let mut errors = Vec::new();
325
326 if let Some(spec_inline_types) = self.inline_type_definitions.get(spec) {
327 for (fact_ref, type_def) in spec_inline_types {
328 let mut visited = HashSet::new();
329 match self.resolve_inline_type_definition(spec, type_def, &mut visited) {
330 Ok(Some(resolved_type)) => {
331 existing
332 .inline_type_definitions
333 .insert(fact_ref.clone(), resolved_type);
334 }
335 Ok(None) => {
336 unreachable!(
337 "BUG: registered inline type definition for fact '{}' could not be resolved (spec='{}')",
338 fact_ref, spec.name
339 );
340 }
341 Err(es) => return Err(es),
342 }
343 }
344 }
345
346 if let Some(spec_inline_defs) = self.inline_type_definitions.get(spec) {
347 for (fact_ref, type_def) in spec_inline_defs {
348 let Some(resolved_type) = existing.inline_type_definitions.get(fact_ref) else {
349 continue;
350 };
351 let e: Result<(), Error> = if resolved_type.is_scale() {
352 Self::add_scale_units_to_index(
353 spec,
354 &mut existing.unit_index,
355 resolved_type,
356 type_def,
357 )
358 } else if resolved_type.is_ratio() {
359 Self::add_ratio_units_to_index(
360 spec,
361 &mut existing.unit_index,
362 resolved_type,
363 type_def,
364 )
365 } else {
366 Ok(())
367 };
368 if let Err(e) = e {
369 errors.push(e);
370 }
371 }
372 }
373
374 if !errors.is_empty() {
375 return Err(errors);
376 }
377
378 Ok(existing)
379 }
380
381 fn resolve_types_internal(
386 &self,
387 spec: &Arc<LemmaSpec>,
388 include_anonymous: bool,
389 ) -> Result<ResolvedSpecTypes, Vec<Error>> {
390 let mut named_types = HashMap::new();
391 let mut inline_type_definitions = HashMap::new();
392 let mut visited = HashSet::new();
393
394 if let Some(spec_types) = self.named_types.get(spec) {
395 for type_name in spec_types.keys() {
396 match self.resolve_type_internal(spec, type_name, &mut visited) {
397 Ok(Some(resolved_type)) => {
398 named_types.insert(type_name.clone(), resolved_type);
399 }
400 Ok(None) => {
401 unreachable!(
402 "BUG: registered named type '{}' could not be resolved (spec='{}')",
403 type_name, spec.name
404 );
405 }
406 Err(es) => return Err(es),
407 }
408 visited.clear();
409 }
410 }
411
412 if include_anonymous {
413 if let Some(spec_inline_types) = self.inline_type_definitions.get(spec) {
414 for (fact_ref, type_def) in spec_inline_types {
415 let mut visited = HashSet::new();
416 match self.resolve_inline_type_definition(spec, type_def, &mut visited) {
417 Ok(Some(resolved_type)) => {
418 inline_type_definitions.insert(fact_ref.clone(), resolved_type);
419 }
420 Ok(None) => {
421 unreachable!(
422 "BUG: registered inline type definition for fact '{}' could not be resolved (spec='{}')",
423 fact_ref, spec.name
424 );
425 }
426 Err(es) => return Err(es),
427 }
428 }
429 }
430 }
431
432 let mut unit_index: HashMap<String, (LemmaType, Option<TypeDef>)> = HashMap::new();
433 let mut errors = Vec::new();
434
435 let prim_ratio = semantics::primitive_ratio();
436 for unit in Self::extract_units_from_type(&prim_ratio.specifications) {
437 unit_index.insert(unit, (prim_ratio.clone(), None));
438 }
439
440 for (type_name, resolved_type) in &named_types {
441 let type_def = self
442 .named_types
443 .get(spec)
444 .and_then(|defs| defs.get(type_name.as_str()))
445 .expect("BUG: named type was resolved but not in registry");
446 let e: Result<(), Error> = if resolved_type.is_scale() {
447 Self::add_scale_units_to_index(spec, &mut unit_index, resolved_type, type_def)
448 } else if resolved_type.is_ratio() {
449 Self::add_ratio_units_to_index(spec, &mut unit_index, resolved_type, type_def)
450 } else {
451 Ok(())
452 };
453 if let Err(e) = e {
454 errors.push(e);
455 }
456 }
457
458 for (fact_ref, resolved_type) in &inline_type_definitions {
459 let type_def = self
460 .inline_type_definitions
461 .get(spec)
462 .and_then(|defs| defs.get(fact_ref))
463 .expect("BUG: inline type was resolved but not in registry");
464 let e: Result<(), Error> = if resolved_type.is_scale() {
465 Self::add_scale_units_to_index(spec, &mut unit_index, resolved_type, type_def)
466 } else if resolved_type.is_ratio() {
467 Self::add_ratio_units_to_index(spec, &mut unit_index, resolved_type, type_def)
468 } else {
469 Ok(())
470 };
471 if let Err(e) = e {
472 errors.push(e);
473 }
474 }
475
476 if !errors.is_empty() {
477 return Err(errors);
478 }
479
480 Ok(ResolvedSpecTypes {
481 named_types,
482 inline_type_definitions,
483 unit_index,
484 })
485 }
486
487 fn resolve_type_internal(
488 &self,
489 spec: &Arc<LemmaSpec>,
490 name: &str,
491 visited: &mut HashSet<String>,
492 ) -> Result<Option<LemmaType>, Vec<Error>> {
493 let key = format!("{}::{}", spec.name, name);
494 if visited.contains(&key) {
495 let source_location = self
496 .named_types
497 .get(spec)
498 .and_then(|dt| dt.get(name))
499 .map(|td| td.source_location().clone())
500 .unwrap_or_else(|| {
501 unreachable!(
502 "BUG: circular dependency detected for type '{}::{}' but type definition not found in registry",
503 spec.name, name
504 )
505 });
506 return Err(vec![Error::validation_with_context(
507 format!("Circular dependency detected in type resolution: {}", key),
508 Some(source_location),
509 None::<String>,
510 Some(Arc::clone(spec)),
511 None,
512 )]);
513 }
514 visited.insert(key.clone());
515
516 let type_def = match self.named_types.get(spec).and_then(|dt| dt.get(name)) {
517 Some(def) => def.clone(),
518 None => {
519 visited.remove(&key);
520 return Ok(None);
521 }
522 };
523
524 let (parent, from, constraints, type_name) = match &type_def {
525 TypeDef::Regular {
526 name,
527 parent,
528 constraints,
529 ..
530 } => (parent.clone(), None, constraints.clone(), name.clone()),
531 TypeDef::Import {
532 name,
533 source_type,
534 from,
535 constraints,
536 ..
537 } => (
538 ParentType::Custom {
539 name: source_type.clone(),
540 },
541 Some(from.clone()),
542 constraints.clone(),
543 name.clone(),
544 ),
545 TypeDef::Inline { .. } => {
546 visited.remove(&key);
547 return Ok(None);
548 }
549 };
550
551 let parent_specs = match self.resolve_parent(
552 spec,
553 &parent,
554 &from,
555 visited,
556 type_def.source_location(),
557 ) {
558 Ok(Some(specs)) => specs,
559 Ok(None) => {
560 visited.remove(&key);
561 let source = type_def.source_location().clone();
562 return Err(vec![Error::validation_with_context(
563 format!("Unknown type: '{}'. Type must be defined before use. Valid primitive types are: boolean, scale, number, ratio, text, date, time, duration, percent", parent),
564 Some(source.clone()),
565 None::<String>,
566 Some(Arc::clone(spec)),
567 None,
568 )]);
569 }
570 Err(es) => {
571 visited.remove(&key);
572 return Err(es);
573 }
574 };
575
576 let final_specs = if let Some(constraints) = &constraints {
577 match Self::apply_constraints(
578 spec,
579 parent_specs,
580 constraints,
581 type_def.source_location(),
582 ) {
583 Ok(specs) => specs,
584 Err(errors) => {
585 visited.remove(&key);
586 return Err(errors);
587 }
588 }
589 } else {
590 parent_specs
591 };
592
593 visited.remove(&key);
594
595 let extends = if matches!(parent, ParentType::Primitive { .. }) {
596 TypeExtends::Primitive
597 } else {
598 let parent_name = match &parent {
599 ParentType::Custom { name } => name.clone(),
600 ParentType::Primitive { .. } => unreachable!("already handled above"),
601 };
602 let parent_spec = match self.get_spec_arc_for_parent(spec, &from) {
603 Ok(x) => x,
604 Err(e) => return Err(vec![e]),
605 };
606 let family = match &parent_spec {
607 Some(r) => match self.resolve_type_internal(&r.spec, &parent_name, visited) {
608 Ok(Some(parent_type)) => parent_type
609 .scale_family_name()
610 .map(String::from)
611 .unwrap_or_else(|| parent_name.clone()),
612 Ok(None) => parent_name.clone(),
613 Err(es) => return Err(es),
614 },
615 None => parent_name.clone(),
616 };
617 let defining_spec = if from.is_some() {
618 match &parent_spec {
619 Some(r) => match &r.resolved_plan_hash {
620 Some(hash) => TypeDefiningSpec::Import {
621 spec: Arc::clone(&r.spec),
622 resolved_plan_hash: hash.clone(),
623 },
624 None => unreachable!(
625 "BUG: from.is_some() but get_spec_arc_for_parent returned None for hash"
626 ),
627 },
628 None => unreachable!(
629 "BUG: from.is_some() but get_spec_arc_for_parent returned Ok(None)"
630 ),
631 }
632 } else {
633 TypeDefiningSpec::Local
634 };
635 TypeExtends::Custom {
636 parent: parent_name,
637 family,
638 defining_spec,
639 }
640 };
641
642 Ok(Some(LemmaType {
643 name: Some(type_name),
644 specifications: final_specs,
645 extends,
646 }))
647 }
648
649 fn resolve_parent(
650 &self,
651 spec: &Arc<LemmaSpec>,
652 parent: &ParentType,
653 from: &Option<crate::parsing::ast::SpecRef>,
654 visited: &mut HashSet<String>,
655 source: &crate::Source,
656 ) -> Result<Option<TypeSpecification>, Vec<Error>> {
657 if let ParentType::Primitive { primitive: kind } = parent {
658 return Ok(Some(semantics::type_spec_for_primitive(*kind)));
659 }
660
661 let parent_name = match parent {
662 ParentType::Custom { name } => name.as_str(),
663 ParentType::Primitive { .. } => unreachable!("already returned above"),
664 };
665
666 let parent_spec = match self.get_spec_arc_for_parent(spec, from) {
667 Ok(x) => x,
668 Err(e) => return Err(vec![e]),
669 };
670 let result = match &parent_spec {
671 Some(r) => self.resolve_type_internal(&r.spec, parent_name, visited),
672 None => Ok(None),
673 };
674 match result {
675 Ok(Some(t)) => Ok(Some(t.specifications)),
676 Ok(None) => {
677 let type_exists = parent_spec
678 .as_ref()
679 .and_then(|r| self.named_types.get(&r.spec))
680 .map(|spec_types| spec_types.contains_key(parent_name))
681 .unwrap_or(false);
682
683 if !type_exists {
684 let suggestion = from.as_ref().filter(|r| r.from_registry).map(|r| {
685 format!(
686 "Run `lemma get` or `lemma get {}` to fetch this dependency.",
687 r.name
688 )
689 });
690 Err(vec![Error::validation_with_context(
691 format!("Unknown type: '{}'. Type must be defined before use. Valid primitive types are: boolean, scale, number, ratio, text, date, time, duration, percent", parent),
692 Some(source.clone()),
693 suggestion,
694 Some(Arc::clone(spec)),
695 None,
696 )])
697 } else {
698 Ok(None)
699 }
700 }
701 Err(es) => Err(es),
702 }
703 }
704
705 fn get_spec_arc_for_parent(
709 &self,
710 spec: &Arc<LemmaSpec>,
711 from: &Option<crate::parsing::ast::SpecRef>,
712 ) -> Result<Option<ResolvedParentSpec>, Error> {
713 match from {
714 Some(from_ref) => self.resolve_spec_for_import(from_ref).map(|(arc, hash)| {
715 Some(ResolvedParentSpec {
716 spec: arc,
717 resolved_plan_hash: Some(hash),
718 })
719 }),
720 None => Ok(Some(ResolvedParentSpec {
721 spec: Arc::clone(spec),
722 resolved_plan_hash: None,
723 })),
724 }
725 }
726
727 fn resolve_spec_for_import(
730 &self,
731 from: &crate::parsing::ast::SpecRef,
732 ) -> Result<(Arc<LemmaSpec>, String), Error> {
733 if let Some(pin) = &from.hash_pin {
734 return match self.plan_hashes.get_by_pin(&from.name, pin) {
735 Some(arc) => {
736 if let Some(err) = super::validate_effective_for_pin(from, arc, self.context) {
737 return Err(err);
738 }
739 Ok((Arc::clone(arc), pin.clone()))
740 }
741 None => Err(Error::validation(
742 format!(
743 "No spec '{}' found with plan hash '{}' for type import",
744 from.name, pin
745 ),
746 None,
747 None::<String>,
748 )),
749 };
750 }
751
752 let at = from.effective.as_ref().or(self.resolve_at.as_ref());
753 let resolved = match at {
754 Some(dt) => self.context.get_spec(&from.name, dt),
755 None => self.context.specs_for_name(&from.name).into_iter().next(),
756 };
757 let arc = resolved.ok_or_else(|| {
758 Error::validation(
759 format!("Spec '{}' not found for type import", from.name),
760 None,
761 None::<String>,
762 )
763 })?;
764 let hash = self
765 .plan_hashes
766 .get_by_slice(&arc.name, &arc.effective_from)
767 .map(std::string::ToString::to_string)
768 .ok_or_else(|| {
769 Error::validation(
770 format!(
771 "Cannot import types from spec '{}': no plan hash (that spec may have failed validation)",
772 arc.name
773 ),
774 None,
775 None::<String>,
776 )
777 })?;
778 Ok((arc, hash))
779 }
780
781 fn apply_constraints(
782 spec: &Arc<LemmaSpec>,
783 mut specs: TypeSpecification,
784 constraints: &[Constraint],
785 source: &crate::Source,
786 ) -> Result<TypeSpecification, Vec<Error>> {
787 let mut errors = Vec::new();
788 for (command, args) in constraints {
789 let specs_clone = specs.clone();
790 match specs.apply_constraint(*command, args) {
791 Ok(updated_specs) => specs = updated_specs,
792 Err(e) => {
793 errors.push(Error::validation_with_context(
794 format!("Failed to apply constraint '{}': {}", command, e),
795 Some(source.clone()),
796 None::<String>,
797 Some(Arc::clone(spec)),
798 None,
799 ));
800 specs = specs_clone;
801 }
802 }
803 }
804 if !errors.is_empty() {
805 return Err(errors);
806 }
807 Ok(specs)
808 }
809
810 fn resolve_inline_type_definition(
811 &self,
812 spec: &Arc<LemmaSpec>,
813 type_def: &TypeDef,
814 visited: &mut HashSet<String>,
815 ) -> Result<Option<LemmaType>, Vec<Error>> {
816 let def_loc = type_def.source_location().clone();
817 let TypeDef::Inline {
818 parent,
819 constraints,
820 fact_ref: _,
821 from,
822 ..
823 } = type_def
824 else {
825 return Ok(None);
826 };
827
828 let parent_specs = match self.resolve_parent(spec, parent, from, visited, &def_loc) {
829 Ok(Some(specs)) => specs,
830 Ok(None) => {
831 return Err(vec![Error::validation_with_context(
832 format!("Unknown type: '{}'. Type must be defined before use. Valid primitive types are: boolean, scale, number, ratio, text, date, time, duration, percent", parent),
833 Some(def_loc.clone()),
834 None::<String>,
835 Some(Arc::clone(spec)),
836 None,
837 )]);
838 }
839 Err(es) => return Err(es),
840 };
841
842 let final_specs = if let Some(constraints) = constraints {
843 Self::apply_constraints(spec, parent_specs, constraints, &def_loc)?
844 } else {
845 parent_specs
846 };
847
848 let extends = if matches!(parent, ParentType::Primitive { .. }) {
849 TypeExtends::Primitive
850 } else {
851 let parent_name = match parent {
852 ParentType::Custom { ref name } => name.clone(),
853 ParentType::Primitive { .. } => unreachable!("already handled above"),
854 };
855 let parent_spec = match self.get_spec_arc_for_parent(spec, from) {
856 Ok(x) => x,
857 Err(e) => return Err(vec![e]),
858 };
859 let family = match &parent_spec {
860 Some(r) => match self.resolve_type_internal(&r.spec, &parent_name, visited) {
861 Ok(Some(parent_type)) => parent_type
862 .scale_family_name()
863 .map(String::from)
864 .unwrap_or_else(|| parent_name.clone()),
865 Ok(None) => parent_name.clone(),
866 Err(es) => return Err(es),
867 },
868 None => parent_name.clone(),
869 };
870 let defining_spec = if from.is_some() {
871 match &parent_spec {
872 Some(r) => match &r.resolved_plan_hash {
873 Some(hash) => TypeDefiningSpec::Import {
874 spec: Arc::clone(&r.spec),
875 resolved_plan_hash: hash.clone(),
876 },
877 None => unreachable!(
878 "BUG: from.is_some() but get_spec_arc_for_parent returned None for hash"
879 ),
880 },
881 None => unreachable!(
882 "BUG: from.is_some() but get_spec_arc_for_parent returned Ok(None)"
883 ),
884 }
885 } else {
886 TypeDefiningSpec::Local
887 };
888 TypeExtends::Custom {
889 parent: parent_name,
890 family,
891 defining_spec,
892 }
893 };
894
895 Ok(Some(LemmaType::without_name(final_specs, extends)))
896 }
897
898 fn add_scale_units_to_index(
903 spec: &Arc<LemmaSpec>,
904 unit_index: &mut HashMap<String, (LemmaType, Option<TypeDef>)>,
905 resolved_type: &LemmaType,
906 defined_by: &TypeDef,
907 ) -> Result<(), Error> {
908 let units = Self::extract_units_from_type(&resolved_type.specifications);
909 for unit in units {
910 if let Some((existing_type, existing_def)) = unit_index.get(&unit) {
911 let same_type = existing_def.as_ref() == Some(defined_by);
912
913 if same_type {
914 return Err(Error::validation_with_context(
915 format!(
916 "Unit '{}' is defined more than once in type '{}'",
917 unit,
918 defined_by.name()
919 ),
920 Some(defined_by.source_location().clone()),
921 None::<String>,
922 Some(Arc::clone(spec)),
923 None,
924 ));
925 }
926
927 let existing_name: String = existing_def
928 .as_ref()
929 .map(|d| d.name().to_owned())
930 .unwrap_or_else(|| existing_type.name());
931 let current_extends_existing = resolved_type
932 .extends
933 .parent_name()
934 .map(|p| p == existing_name.as_str())
935 .unwrap_or(false);
936 let existing_extends_current = existing_type
937 .extends
938 .parent_name()
939 .map(|p| p == defined_by.name())
940 .unwrap_or(false);
941
942 if existing_type.is_scale()
943 && (current_extends_existing || existing_extends_current)
944 {
945 if current_extends_existing {
946 unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
947 }
948 continue;
949 }
950
951 if existing_type.same_scale_family(resolved_type) {
952 continue;
953 }
954
955 return Err(Error::validation_with_context(
956 format!(
957 "Ambiguous unit '{}'. Defined in multiple types: '{}' and '{}'",
958 unit,
959 existing_name,
960 defined_by.name()
961 ),
962 Some(defined_by.source_location().clone()),
963 None::<String>,
964 Some(Arc::clone(spec)),
965 None,
966 ));
967 }
968 unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
969 }
970 Ok(())
971 }
972
973 fn add_ratio_units_to_index(
974 spec: &Arc<LemmaSpec>,
975 unit_index: &mut HashMap<String, (LemmaType, Option<TypeDef>)>,
976 resolved_type: &LemmaType,
977 defined_by: &TypeDef,
978 ) -> Result<(), Error> {
979 let units = Self::extract_units_from_type(&resolved_type.specifications);
980 for unit in units {
981 if let Some((existing_type, existing_def)) = unit_index.get(&unit) {
982 if existing_type.is_ratio() {
983 continue;
984 }
985 let existing_name: String = existing_def
986 .as_ref()
987 .map(|d| d.name().to_owned())
988 .unwrap_or_else(|| existing_type.name());
989 return Err(Error::validation_with_context(
990 format!(
991 "Ambiguous unit '{}'. Defined in multiple types: '{}' and '{}'",
992 unit,
993 existing_name,
994 defined_by.name()
995 ),
996 Some(defined_by.source_location().clone()),
997 None::<String>,
998 Some(Arc::clone(spec)),
999 None,
1000 ));
1001 }
1002 unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
1003 }
1004 Ok(())
1005 }
1006
1007 fn extract_units_from_type(specs: &TypeSpecification) -> Vec<String> {
1008 match specs {
1009 TypeSpecification::Scale { units, .. } => {
1010 units.iter().map(|unit| unit.name.clone()).collect()
1011 }
1012 TypeSpecification::Ratio { units, .. } => {
1013 units.iter().map(|unit| unit.name.clone()).collect()
1014 }
1015 _ => Vec::new(),
1016 }
1017 }
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 use super::*;
1023 use crate::engine::Context;
1024 use crate::parse;
1025 use crate::parsing::ast::{
1026 CommandArg, LemmaSpec, ParentType, PrimitiveKind, TypeConstraintCommand,
1027 };
1028 use crate::ResourceLimits;
1029 use rust_decimal::Decimal;
1030 use std::sync::Arc;
1031
1032 fn test_context_and_spec() -> (Context, Arc<LemmaSpec>) {
1033 let spec = LemmaSpec::new("test_spec".to_string());
1034 let arc = Arc::new(spec);
1035 let mut ctx = Context::new();
1036 ctx.insert_spec(Arc::clone(&arc), false)
1037 .expect("insert test spec");
1038 (ctx, arc)
1039 }
1040
1041 fn resolver_for_code(code: &str) -> (PerSliceTypeResolver<'static>, Vec<Arc<LemmaSpec>>) {
1042 let specs = parse(code, "test.lemma", &ResourceLimits::default())
1045 .unwrap()
1046 .specs;
1047 let ctx = Box::leak(Box::new(Context::new()));
1048 let mut spec_arcs = Vec::new();
1049 for spec in &specs {
1050 let arc = Arc::new(spec.clone());
1051 ctx.insert_spec(Arc::clone(&arc), spec.from_registry)
1052 .expect("insert spec");
1053 spec_arcs.push(arc);
1054 }
1055 let plan_hashes = Box::leak(Box::new(crate::planning::PlanHashRegistry::default()));
1056 let mut resolver = PerSliceTypeResolver::new(ctx, None, plan_hashes);
1057 for spec_arc in &spec_arcs {
1058 resolver.register_all(spec_arc);
1059 }
1060 (resolver, spec_arcs)
1061 }
1062
1063 fn resolver_single_spec(code: &str) -> (PerSliceTypeResolver<'static>, Arc<LemmaSpec>) {
1064 let (resolver, spec_arcs) = resolver_for_code(code);
1065 let spec_arc = spec_arcs.into_iter().next().expect("at least one spec");
1066 (resolver, spec_arc)
1067 }
1068
1069 #[test]
1070 fn test_registry_creation() {
1071 let (ctx, spec_arc) = test_context_and_spec();
1072 let ph = crate::planning::PlanHashRegistry::default();
1073 let resolver = PerSliceTypeResolver::new(&ctx, None, &ph);
1074 let resolved = resolver.resolve_named_types(&spec_arc).unwrap();
1075 assert!(resolved.named_types.is_empty());
1076 assert!(resolved.inline_type_definitions.is_empty());
1077 }
1078
1079 #[test]
1080 fn test_type_spec_for_primitive_covers_all_variants() {
1081 use crate::parsing::ast::PrimitiveKind;
1082 use crate::planning::semantics::type_spec_for_primitive;
1083
1084 for kind in [
1085 PrimitiveKind::Boolean,
1086 PrimitiveKind::Scale,
1087 PrimitiveKind::Number,
1088 PrimitiveKind::Percent,
1089 PrimitiveKind::Ratio,
1090 PrimitiveKind::Text,
1091 PrimitiveKind::Date,
1092 PrimitiveKind::Time,
1093 PrimitiveKind::Duration,
1094 ] {
1095 let spec = type_spec_for_primitive(kind);
1096 assert!(
1097 !matches!(
1098 spec,
1099 crate::planning::semantics::TypeSpecification::Undetermined
1100 ),
1101 "type_spec_for_primitive({:?}) returned Undetermined",
1102 kind
1103 );
1104 }
1105 }
1106
1107 #[test]
1108 fn test_register_named_type() {
1109 let (ctx, spec_arc) = test_context_and_spec();
1110 let ph = crate::planning::PlanHashRegistry::default();
1111 let mut resolver = PerSliceTypeResolver::new(&ctx, None, &ph);
1112 let type_def = TypeDef::Regular {
1113 source_location: crate::Source::new(
1114 "<test>",
1115 crate::parsing::ast::Span {
1116 start: 0,
1117 end: 0,
1118 line: 1,
1119 col: 0,
1120 },
1121 ),
1122 name: "money".to_string(),
1123 parent: ParentType::Primitive {
1124 primitive: PrimitiveKind::Number,
1125 },
1126 constraints: None,
1127 };
1128
1129 let result = resolver.register_type(&spec_arc, type_def);
1130 assert!(result.is_ok());
1131 }
1132
1133 #[test]
1134 fn test_register_inline_type_definition() {
1135 use crate::parsing::ast::Reference;
1136 let (ctx, spec_arc) = test_context_and_spec();
1137 let ph = crate::planning::PlanHashRegistry::default();
1138 let mut resolver = PerSliceTypeResolver::new(&ctx, None, &ph);
1139 let fact_ref = Reference::local("age".to_string());
1140 let type_def = TypeDef::Inline {
1141 source_location: crate::Source::new(
1142 "<test>",
1143 crate::parsing::ast::Span {
1144 start: 0,
1145 end: 0,
1146 line: 1,
1147 col: 0,
1148 },
1149 ),
1150 parent: ParentType::Primitive {
1151 primitive: PrimitiveKind::Number,
1152 },
1153 constraints: Some(vec![
1154 (
1155 TypeConstraintCommand::Minimum,
1156 vec![CommandArg::Number("0".to_string())],
1157 ),
1158 (
1159 TypeConstraintCommand::Maximum,
1160 vec![CommandArg::Number("150".to_string())],
1161 ),
1162 ]),
1163 fact_ref: fact_ref.clone(),
1164 from: None,
1165 };
1166
1167 let result = resolver.register_type(&spec_arc, type_def);
1168 assert!(result.is_ok());
1169 let resolved = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1170 assert!(resolved.inline_type_definitions.contains_key(&fact_ref));
1171 }
1172
1173 #[test]
1174 fn test_register_duplicate_type_fails() {
1175 let (ctx, spec_arc) = test_context_and_spec();
1176 let ph = crate::planning::PlanHashRegistry::default();
1177 let mut resolver = PerSliceTypeResolver::new(&ctx, None, &ph);
1178 let type_def = TypeDef::Regular {
1179 source_location: crate::Source::new(
1180 "<test>",
1181 crate::parsing::ast::Span {
1182 start: 0,
1183 end: 0,
1184 line: 1,
1185 col: 0,
1186 },
1187 ),
1188 name: "money".to_string(),
1189 parent: ParentType::Primitive {
1190 primitive: PrimitiveKind::Number,
1191 },
1192 constraints: None,
1193 };
1194
1195 resolver.register_type(&spec_arc, type_def.clone()).unwrap();
1196 let result = resolver.register_type(&spec_arc, type_def);
1197 assert!(result.is_err());
1198 }
1199
1200 #[test]
1201 fn test_resolve_custom_type_from_primitive() {
1202 let (ctx, spec_arc) = test_context_and_spec();
1203 let ph = crate::planning::PlanHashRegistry::default();
1204 let mut resolver = PerSliceTypeResolver::new(&ctx, None, &ph);
1205 let type_def = TypeDef::Regular {
1206 source_location: crate::Source::new(
1207 "<test>",
1208 crate::parsing::ast::Span {
1209 start: 0,
1210 end: 0,
1211 line: 1,
1212 col: 0,
1213 },
1214 ),
1215 name: "money".to_string(),
1216 parent: ParentType::Primitive {
1217 primitive: PrimitiveKind::Number,
1218 },
1219 constraints: None,
1220 };
1221
1222 resolver.register_type(&spec_arc, type_def).unwrap();
1223 let resolved = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1224
1225 assert!(resolved.named_types.contains_key("money"));
1226 let money_type = resolved.named_types.get("money").unwrap();
1227 assert_eq!(money_type.name, Some("money".to_string()));
1228 }
1229
1230 #[test]
1231 fn test_type_definition_resolution() {
1232 let (resolver, spec_arc) = resolver_single_spec(
1233 r#"spec test
1234type dice: number -> minimum 0 -> maximum 6"#,
1235 );
1236
1237 let resolved_types = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1238 let dice_type = resolved_types.named_types.get("dice").unwrap();
1239
1240 match &dice_type.specifications {
1241 TypeSpecification::Number {
1242 minimum, maximum, ..
1243 } => {
1244 assert_eq!(*minimum, Some(Decimal::from(0)));
1245 assert_eq!(*maximum, Some(Decimal::from(6)));
1246 }
1247 _ => panic!("Expected Number type specifications"),
1248 }
1249 }
1250
1251 #[test]
1252 fn test_type_definition_with_multiple_commands() {
1253 let (resolver, spec_arc) = resolver_single_spec(
1254 r#"spec test
1255type money: scale -> decimals 2 -> unit eur 1.0 -> unit usd 1.18"#,
1256 );
1257
1258 let resolved_types = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1259 let money_type = resolved_types.named_types.get("money").unwrap();
1260
1261 match &money_type.specifications {
1262 TypeSpecification::Scale {
1263 decimals, units, ..
1264 } => {
1265 assert_eq!(*decimals, Some(2));
1266 assert_eq!(units.len(), 2);
1267 assert!(units.iter().any(|u| u.name == "eur"));
1268 assert!(units.iter().any(|u| u.name == "usd"));
1269 }
1270 _ => panic!("Expected Scale type specifications"),
1271 }
1272 }
1273
1274 #[test]
1275 fn test_number_type_with_decimals() {
1276 let (resolver, spec_arc) = resolver_single_spec(
1277 r#"spec test
1278type price: number -> decimals 2 -> minimum 0"#,
1279 );
1280
1281 let resolved_types = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1282 let price_type = resolved_types.named_types.get("price").unwrap();
1283
1284 match &price_type.specifications {
1285 TypeSpecification::Number {
1286 decimals, minimum, ..
1287 } => {
1288 assert_eq!(*decimals, Some(2));
1289 assert_eq!(*minimum, Some(Decimal::from(0)));
1290 }
1291 _ => panic!("Expected Number type specifications with decimals"),
1292 }
1293 }
1294
1295 #[test]
1296 fn test_number_type_decimals_only() {
1297 let (resolver, spec_arc) = resolver_single_spec(
1298 r#"spec test
1299type precise_number: number -> decimals 4"#,
1300 );
1301
1302 let resolved_types = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1303 let precise_type = resolved_types.named_types.get("precise_number").unwrap();
1304
1305 match &precise_type.specifications {
1306 TypeSpecification::Number { decimals, .. } => {
1307 assert_eq!(*decimals, Some(4));
1308 }
1309 _ => panic!("Expected Number type with decimals 4"),
1310 }
1311 }
1312
1313 #[test]
1314 fn test_scale_type_decimals_only() {
1315 let (resolver, spec_arc) = resolver_single_spec(
1316 r#"spec test
1317type weight: scale -> unit kg 1 -> decimals 3"#,
1318 );
1319
1320 let resolved_types = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1321 let weight_type = resolved_types.named_types.get("weight").unwrap();
1322
1323 match &weight_type.specifications {
1324 TypeSpecification::Scale { decimals, .. } => {
1325 assert_eq!(*decimals, Some(3));
1326 }
1327 _ => panic!("Expected Scale type with decimals 3"),
1328 }
1329 }
1330
1331 #[test]
1332 fn test_ratio_type_accepts_optional_decimals_command() {
1333 let (resolver, spec_arc) = resolver_single_spec(
1334 r#"spec test
1335type ratio_type: ratio -> decimals 2"#,
1336 );
1337
1338 let resolved_types = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1339 let ratio_type = resolved_types.named_types.get("ratio_type").unwrap();
1340
1341 match &ratio_type.specifications {
1342 TypeSpecification::Ratio { decimals, .. } => {
1343 assert_eq!(
1344 *decimals,
1345 Some(2),
1346 "ratio type should accept decimals command"
1347 );
1348 }
1349 _ => panic!("Expected Ratio type with decimals 2"),
1350 }
1351 }
1352
1353 #[test]
1354 fn test_ratio_type_with_default_command() {
1355 let (resolver, spec_arc) = resolver_single_spec(
1356 r#"spec test
1357type percentage: ratio -> minimum 0 -> maximum 1 -> default 0.5"#,
1358 );
1359
1360 let resolved_types = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1361 let percentage_type = resolved_types.named_types.get("percentage").unwrap();
1362
1363 match &percentage_type.specifications {
1364 TypeSpecification::Ratio {
1365 minimum,
1366 maximum,
1367 default,
1368 ..
1369 } => {
1370 assert_eq!(
1371 *minimum,
1372 Some(Decimal::from(0)),
1373 "ratio type should have minimum 0"
1374 );
1375 assert_eq!(
1376 *maximum,
1377 Some(Decimal::from(1)),
1378 "ratio type should have maximum 1"
1379 );
1380 assert_eq!(
1381 *default,
1382 Some(Decimal::from_i128_with_scale(5, 1)),
1383 "ratio type with default command must work"
1384 );
1385 }
1386 _ => panic!("Expected Ratio type with minimum, maximum, and default"),
1387 }
1388 }
1389
1390 #[test]
1391 fn test_scale_extension_chain_same_family_units_allowed() {
1392 let (resolver, spec_arc) = resolver_single_spec(
1393 r#"spec test
1394type money: scale -> unit eur 1
1395type money2: money -> unit usd 1.24"#,
1396 );
1397
1398 let result = resolver.resolve_types_internal(&spec_arc, true);
1399 assert!(
1400 result.is_ok(),
1401 "Scale extension chain should resolve: {:?}",
1402 result.err()
1403 );
1404
1405 let resolved = result.unwrap();
1406 assert!(
1407 resolved.unit_index.contains_key("eur"),
1408 "eur should be in unit_index"
1409 );
1410 assert!(
1411 resolved.unit_index.contains_key("usd"),
1412 "usd should be in unit_index"
1413 );
1414 let (eur_type, _) = resolved.unit_index.get("eur").unwrap();
1415 let (usd_type, _) = resolved.unit_index.get("usd").unwrap();
1416 assert_eq!(
1417 eur_type.name.as_deref(),
1418 Some("money2"),
1419 "more derived type (money2) should own eur for conversion"
1420 );
1421 assert_eq!(usd_type.name.as_deref(), Some("money2"));
1422 }
1423
1424 #[test]
1425 fn test_invalid_parent_type_in_named_type_should_error() {
1426 let (resolver, spec_arc) = resolver_single_spec(
1427 r#"spec test
1428type invalid: nonexistent_type -> minimum 0"#,
1429 );
1430
1431 let result = resolver.resolve_types_internal(&spec_arc, true);
1432 assert!(result.is_err(), "Should reject invalid parent type");
1433
1434 let errs = result.unwrap_err();
1435 assert!(!errs.is_empty(), "expected at least one error");
1436 let error_msg = errs[0].to_string();
1437 assert!(
1438 error_msg.contains("Unknown type") && error_msg.contains("nonexistent_type"),
1439 "Error should mention unknown type. Got: {}",
1440 error_msg
1441 );
1442 }
1443
1444 #[test]
1445 fn test_invalid_primitive_type_name_should_error() {
1446 let (resolver, spec_arc) = resolver_single_spec(
1447 r#"spec test
1448type invalid: choice -> option "a""#,
1449 );
1450
1451 let result = resolver.resolve_types_internal(&spec_arc, true);
1452 assert!(result.is_err(), "Should reject invalid type base 'choice'");
1453
1454 let errs = result.unwrap_err();
1455 assert!(!errs.is_empty(), "expected at least one error");
1456 let error_msg = errs[0].to_string();
1457 assert!(
1458 error_msg.contains("Unknown type") && error_msg.contains("choice"),
1459 "Error should mention unknown type 'choice'. Got: {}",
1460 error_msg
1461 );
1462 }
1463
1464 #[test]
1465 fn test_unit_constraint_validation_errors_are_reported() {
1466 let (resolver, spec_arc) = resolver_single_spec(
1467 r#"spec test
1468type money: scale
1469 -> unit eur 1.00
1470 -> unit usd 1.19
1471
1472type money2: money
1473 -> unit eur 1.20
1474 -> unit usd 1.21
1475 -> unit gbp 1.30"#,
1476 );
1477
1478 let result = resolver.resolve_types_internal(&spec_arc, true);
1479 assert!(
1480 result.is_err(),
1481 "Expected unit constraint conflicts to error"
1482 );
1483
1484 let errs = result.unwrap_err();
1485 assert!(!errs.is_empty(), "expected at least one error");
1486 let error_msg = errs
1487 .iter()
1488 .map(ToString::to_string)
1489 .collect::<Vec<_>>()
1490 .join("; ");
1491 assert!(
1492 error_msg.contains("eur") || error_msg.contains("usd"),
1493 "Error should mention the conflicting units. Got: {}",
1494 error_msg
1495 );
1496 }
1497
1498 #[test]
1499 fn test_spec_level_unit_ambiguity_errors_are_reported() {
1500 let (resolver, spec_arc) = resolver_single_spec(
1501 r#"spec test
1502type money_a: scale
1503 -> unit eur 1.00
1504 -> unit usd 1.19
1505
1506type money_b: scale
1507 -> unit eur 1.00
1508 -> unit usd 1.20
1509
1510type length_a: scale
1511 -> unit meter 1.0
1512
1513type length_b: scale
1514 -> unit meter 1.0"#,
1515 );
1516
1517 let result = resolver.resolve_types_internal(&spec_arc, true);
1518 assert!(
1519 result.is_err(),
1520 "Expected ambiguous unit definitions to error"
1521 );
1522
1523 let errs = result.unwrap_err();
1524 assert!(!errs.is_empty(), "expected at least one error");
1525 let error_msg = errs
1526 .iter()
1527 .map(ToString::to_string)
1528 .collect::<Vec<_>>()
1529 .join("; ");
1530 assert!(
1531 error_msg.contains("eur") || error_msg.contains("usd") || error_msg.contains("meter"),
1532 "Error should mention at least one ambiguous unit. Got: {}",
1533 error_msg
1534 );
1535 }
1536
1537 #[test]
1538 fn test_number_type_cannot_have_units() {
1539 let (resolver, spec_arc) = resolver_single_spec(
1540 r#"spec test
1541type price: number
1542 -> unit eur 1.00"#,
1543 );
1544
1545 let result = resolver.resolve_types_internal(&spec_arc, true);
1546 assert!(result.is_err(), "Number types must reject unit commands");
1547
1548 let errs = result.unwrap_err();
1549 assert!(!errs.is_empty(), "expected at least one error");
1550 let error_msg = errs[0].to_string();
1551 assert!(
1552 error_msg.contains("unit") && error_msg.contains("number"),
1553 "Error should mention units are invalid on number. Got: {}",
1554 error_msg
1555 );
1556 }
1557
1558 #[test]
1559 fn test_scale_type_can_have_units() {
1560 let (resolver, spec_arc) = resolver_single_spec(
1561 r#"spec test
1562type money: scale
1563 -> unit eur 1.00
1564 -> unit usd 1.19"#,
1565 );
1566
1567 let resolved = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1568 let money_type = resolved.named_types.get("money").unwrap();
1569
1570 match &money_type.specifications {
1571 TypeSpecification::Scale { units, .. } => {
1572 assert_eq!(units.len(), 2);
1573 assert!(units.iter().any(|u| u.name == "eur"));
1574 assert!(units.iter().any(|u| u.name == "usd"));
1575 }
1576 other => panic!("Expected Scale type specifications, got {:?}", other),
1577 }
1578 }
1579
1580 #[test]
1581 fn test_extending_type_inherits_units() {
1582 let (resolver, spec_arc) = resolver_single_spec(
1583 r#"spec test
1584type money: scale
1585 -> unit eur 1.00
1586 -> unit usd 1.19
1587
1588type my_money: money
1589 -> unit gbp 1.30"#,
1590 );
1591
1592 let resolved = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1593 let my_money_type = resolved.named_types.get("my_money").unwrap();
1594
1595 match &my_money_type.specifications {
1596 TypeSpecification::Scale { units, .. } => {
1597 assert_eq!(units.len(), 3);
1598 assert!(units.iter().any(|u| u.name == "eur"));
1599 assert!(units.iter().any(|u| u.name == "usd"));
1600 assert!(units.iter().any(|u| u.name == "gbp"));
1601 }
1602 other => panic!("Expected Scale type specifications, got {:?}", other),
1603 }
1604 }
1605
1606 #[test]
1607 fn test_duplicate_unit_in_same_type_is_rejected() {
1608 let (resolver, spec_arc) = resolver_single_spec(
1609 r#"spec test
1610type money: scale
1611 -> unit eur 1.00
1612 -> unit eur 1.19"#,
1613 );
1614
1615 let result = resolver.resolve_types_internal(&spec_arc, true);
1616 assert!(
1617 result.is_err(),
1618 "Duplicate units within a type should error"
1619 );
1620
1621 let errs = result.unwrap_err();
1622 assert!(!errs.is_empty(), "expected at least one error");
1623 let error_msg = errs[0].to_string();
1624 assert!(
1625 error_msg.contains("Duplicate unit")
1626 || error_msg.contains("duplicate")
1627 || error_msg.contains("already exists")
1628 || error_msg.contains("eur"),
1629 "Error should mention duplicate unit issue. Got: {}",
1630 error_msg
1631 );
1632 }
1633
1634 #[test]
1635 fn repro_named_type_source_location_panic() {
1636 use crate::parsing::ast::{CommandArg, ParentType, PrimitiveKind};
1637 let code = r#"spec nettoloon
1638type geld: scale
1639 -> decimals 2
1640 -> unit eur 1.00
1641 -> minimum 0 eur
1642fact bruto_salaris: 0 eur"#;
1643 let (mut resolver, spec_arc) = resolver_single_spec(code);
1644 let fact_ref = Reference::local("bruto_salaris".to_string());
1645 let inline_def = TypeDef::Inline {
1646 source_location: spec_arc.types[0].source_location().clone(),
1647 parent: ParentType::Primitive {
1648 primitive: PrimitiveKind::Scale,
1649 },
1650 constraints: Some(vec![(
1651 TypeConstraintCommand::Unit,
1652 vec![
1653 CommandArg::Label("eur".to_string()),
1654 CommandArg::Number("1.00".to_string()),
1655 ],
1656 )]),
1657 fact_ref: fact_ref.clone(),
1658 from: None,
1659 };
1660 resolver.register_type(&spec_arc, inline_def).unwrap();
1661 let _ = resolver.resolve_types_internal(&spec_arc, true);
1662 }
1663}