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