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(source_type.clone()),
533 Some(from.clone()),
534 constraints.clone(),
535 name.clone(),
536 ),
537 TypeDef::Inline { .. } => {
538 visited.remove(&key);
539 return Ok(None);
540 }
541 };
542
543 let parent_specs = match self.resolve_parent(
544 spec,
545 &parent,
546 &from,
547 visited,
548 type_def.source_location(),
549 ) {
550 Ok(Some(specs)) => specs,
551 Ok(None) => {
552 visited.remove(&key);
553 let source = type_def.source_location().clone();
554 return Err(vec![Error::validation_with_context(
555 format!("Unknown type: '{}'. Type must be defined before use. Valid primitive types are: boolean, scale, number, ratio, text, date, time, duration, percent", parent),
556 Some(source.clone()),
557 None::<String>,
558 Some(Arc::clone(spec)),
559 None,
560 )]);
561 }
562 Err(es) => {
563 visited.remove(&key);
564 return Err(es);
565 }
566 };
567
568 let final_specs = if let Some(constraints) = &constraints {
569 match Self::apply_constraints(
570 spec,
571 parent_specs,
572 constraints,
573 type_def.source_location(),
574 ) {
575 Ok(specs) => specs,
576 Err(errors) => {
577 visited.remove(&key);
578 return Err(errors);
579 }
580 }
581 } else {
582 parent_specs
583 };
584
585 visited.remove(&key);
586
587 let extends = if matches!(parent, ParentType::Primitive(_)) {
588 TypeExtends::Primitive
589 } else {
590 let parent_name = match &parent {
591 ParentType::Custom(name) => name.clone(),
592 ParentType::Primitive(_) => unreachable!("already handled above"),
593 };
594 let parent_spec = match self.get_spec_arc_for_parent(spec, &from) {
595 Ok(x) => x,
596 Err(e) => return Err(vec![e]),
597 };
598 let family = match &parent_spec {
599 Some(r) => match self.resolve_type_internal(&r.spec, &parent_name, visited) {
600 Ok(Some(parent_type)) => parent_type
601 .scale_family_name()
602 .map(String::from)
603 .unwrap_or_else(|| parent_name.clone()),
604 Ok(None) => parent_name.clone(),
605 Err(es) => return Err(es),
606 },
607 None => parent_name.clone(),
608 };
609 let defining_spec = if from.is_some() {
610 match &parent_spec {
611 Some(r) => match &r.resolved_plan_hash {
612 Some(hash) => TypeDefiningSpec::Import {
613 spec: Arc::clone(&r.spec),
614 resolved_plan_hash: hash.clone(),
615 },
616 None => unreachable!(
617 "BUG: from.is_some() but get_spec_arc_for_parent returned None for hash"
618 ),
619 },
620 None => unreachable!(
621 "BUG: from.is_some() but get_spec_arc_for_parent returned Ok(None)"
622 ),
623 }
624 } else {
625 TypeDefiningSpec::Local
626 };
627 TypeExtends::Custom {
628 parent: parent_name,
629 family,
630 defining_spec,
631 }
632 };
633
634 Ok(Some(LemmaType {
635 name: Some(type_name),
636 specifications: final_specs,
637 extends,
638 }))
639 }
640
641 fn resolve_parent(
642 &self,
643 spec: &Arc<LemmaSpec>,
644 parent: &ParentType,
645 from: &Option<crate::parsing::ast::SpecRef>,
646 visited: &mut HashSet<String>,
647 source: &crate::Source,
648 ) -> Result<Option<TypeSpecification>, Vec<Error>> {
649 if let ParentType::Primitive(kind) = parent {
650 return Ok(Some(semantics::type_spec_for_primitive(*kind)));
651 }
652
653 let parent_name = match parent {
654 ParentType::Custom(name) => name.as_str(),
655 ParentType::Primitive(_) => unreachable!("already returned above"),
656 };
657
658 let parent_spec = match self.get_spec_arc_for_parent(spec, from) {
659 Ok(x) => x,
660 Err(e) => return Err(vec![e]),
661 };
662 let result = match &parent_spec {
663 Some(r) => self.resolve_type_internal(&r.spec, parent_name, visited),
664 None => Ok(None),
665 };
666 match result {
667 Ok(Some(t)) => Ok(Some(t.specifications)),
668 Ok(None) => {
669 let type_exists = parent_spec
670 .as_ref()
671 .and_then(|r| self.named_types.get(&r.spec))
672 .map(|spec_types| spec_types.contains_key(parent_name))
673 .unwrap_or(false);
674
675 if !type_exists {
676 let suggestion = from.as_ref().filter(|r| r.from_registry).map(|r| {
677 format!(
678 "Run `lemma get` or `lemma get {}` to fetch this dependency.",
679 r.name
680 )
681 });
682 Err(vec![Error::validation_with_context(
683 format!("Unknown type: '{}'. Type must be defined before use. Valid primitive types are: boolean, scale, number, ratio, text, date, time, duration, percent", parent),
684 Some(source.clone()),
685 suggestion,
686 Some(Arc::clone(spec)),
687 None,
688 )])
689 } else {
690 Ok(None)
691 }
692 }
693 Err(es) => Err(es),
694 }
695 }
696
697 fn get_spec_arc_for_parent(
701 &self,
702 spec: &Arc<LemmaSpec>,
703 from: &Option<crate::parsing::ast::SpecRef>,
704 ) -> Result<Option<ResolvedParentSpec>, Error> {
705 match from {
706 Some(from_ref) => self.resolve_spec_for_import(from_ref).map(|(arc, hash)| {
707 Some(ResolvedParentSpec {
708 spec: arc,
709 resolved_plan_hash: Some(hash),
710 })
711 }),
712 None => Ok(Some(ResolvedParentSpec {
713 spec: Arc::clone(spec),
714 resolved_plan_hash: None,
715 })),
716 }
717 }
718
719 fn resolve_spec_for_import(
722 &self,
723 from: &crate::parsing::ast::SpecRef,
724 ) -> Result<(Arc<LemmaSpec>, String), Error> {
725 if let Some(pin) = &from.hash_pin {
726 return match self.plan_hashes.get_by_pin(&from.name, pin) {
727 Some(arc) => Ok((Arc::clone(arc), pin.clone())),
728 None => Err(Error::validation(
729 format!(
730 "No spec '{}' found with plan hash '{}' for type import",
731 from.name, pin
732 ),
733 None,
734 None::<String>,
735 )),
736 };
737 }
738
739 let at = from.effective.as_ref().or(self.resolve_at.as_ref());
740 let resolved = match at {
741 Some(dt) => self.context.get_spec(&from.name, dt),
742 None => self.context.specs_for_name(&from.name).into_iter().next(),
743 };
744 let arc = resolved.ok_or_else(|| {
745 Error::validation(
746 format!("Spec '{}' not found for type import", from.name),
747 None,
748 None::<String>,
749 )
750 })?;
751 let hash = self
752 .plan_hashes
753 .get_by_slice(&arc.name, &arc.effective_from)
754 .map(std::string::ToString::to_string)
755 .unwrap_or_else(|| {
756 unreachable!(
757 "BUG: resolved type-import dependency must have plan hash; \
758 topological planning guarantees deps are planned first"
759 )
760 });
761 Ok((arc, hash))
762 }
763
764 fn apply_constraints(
765 spec: &Arc<LemmaSpec>,
766 mut specs: TypeSpecification,
767 constraints: &[Constraint],
768 source: &crate::Source,
769 ) -> Result<TypeSpecification, Vec<Error>> {
770 let mut errors = Vec::new();
771 for (command, args) in constraints {
772 let specs_clone = specs.clone();
773 match specs.apply_constraint(*command, args) {
774 Ok(updated_specs) => specs = updated_specs,
775 Err(e) => {
776 errors.push(Error::validation_with_context(
777 format!("Failed to apply constraint '{}': {}", command, e),
778 Some(source.clone()),
779 None::<String>,
780 Some(Arc::clone(spec)),
781 None,
782 ));
783 specs = specs_clone;
784 }
785 }
786 }
787 if !errors.is_empty() {
788 return Err(errors);
789 }
790 Ok(specs)
791 }
792
793 fn resolve_inline_type_definition(
794 &self,
795 spec: &Arc<LemmaSpec>,
796 type_def: &TypeDef,
797 visited: &mut HashSet<String>,
798 ) -> Result<Option<LemmaType>, Vec<Error>> {
799 let def_loc = type_def.source_location().clone();
800 let TypeDef::Inline {
801 parent,
802 constraints,
803 fact_ref: _,
804 from,
805 ..
806 } = type_def
807 else {
808 return Ok(None);
809 };
810
811 let parent_specs = match self.resolve_parent(spec, parent, from, visited, &def_loc) {
812 Ok(Some(specs)) => specs,
813 Ok(None) => {
814 return Err(vec![Error::validation_with_context(
815 format!("Unknown type: '{}'. Type must be defined before use. Valid primitive types are: boolean, scale, number, ratio, text, date, time, duration, percent", parent),
816 Some(def_loc.clone()),
817 None::<String>,
818 Some(Arc::clone(spec)),
819 None,
820 )]);
821 }
822 Err(es) => return Err(es),
823 };
824
825 let final_specs = if let Some(constraints) = constraints {
826 Self::apply_constraints(spec, parent_specs, constraints, &def_loc)?
827 } else {
828 parent_specs
829 };
830
831 let extends = if matches!(parent, ParentType::Primitive(_)) {
832 TypeExtends::Primitive
833 } else {
834 let parent_name = match parent {
835 ParentType::Custom(ref name) => name.clone(),
836 ParentType::Primitive(_) => unreachable!("already handled above"),
837 };
838 let parent_spec = match self.get_spec_arc_for_parent(spec, from) {
839 Ok(x) => x,
840 Err(e) => return Err(vec![e]),
841 };
842 let family = match &parent_spec {
843 Some(r) => match self.resolve_type_internal(&r.spec, &parent_name, visited) {
844 Ok(Some(parent_type)) => parent_type
845 .scale_family_name()
846 .map(String::from)
847 .unwrap_or_else(|| parent_name.clone()),
848 Ok(None) => parent_name.clone(),
849 Err(es) => return Err(es),
850 },
851 None => parent_name.clone(),
852 };
853 let defining_spec = if from.is_some() {
854 match &parent_spec {
855 Some(r) => match &r.resolved_plan_hash {
856 Some(hash) => TypeDefiningSpec::Import {
857 spec: Arc::clone(&r.spec),
858 resolved_plan_hash: hash.clone(),
859 },
860 None => unreachable!(
861 "BUG: from.is_some() but get_spec_arc_for_parent returned None for hash"
862 ),
863 },
864 None => unreachable!(
865 "BUG: from.is_some() but get_spec_arc_for_parent returned Ok(None)"
866 ),
867 }
868 } else {
869 TypeDefiningSpec::Local
870 };
871 TypeExtends::Custom {
872 parent: parent_name,
873 family,
874 defining_spec,
875 }
876 };
877
878 Ok(Some(LemmaType::without_name(final_specs, extends)))
879 }
880
881 fn add_scale_units_to_index(
886 spec: &Arc<LemmaSpec>,
887 unit_index: &mut HashMap<String, (LemmaType, Option<TypeDef>)>,
888 resolved_type: &LemmaType,
889 defined_by: &TypeDef,
890 ) -> Result<(), Error> {
891 let units = Self::extract_units_from_type(&resolved_type.specifications);
892 for unit in units {
893 if let Some((existing_type, existing_def)) = unit_index.get(&unit) {
894 let same_type = existing_def.as_ref() == Some(defined_by);
895
896 if same_type {
897 return Err(Error::validation_with_context(
898 format!(
899 "Unit '{}' is defined more than once in type '{}'",
900 unit,
901 defined_by.name()
902 ),
903 Some(defined_by.source_location().clone()),
904 None::<String>,
905 Some(Arc::clone(spec)),
906 None,
907 ));
908 }
909
910 let existing_name: String = existing_def
911 .as_ref()
912 .map(|d| d.name().to_owned())
913 .unwrap_or_else(|| existing_type.name());
914 let current_extends_existing = resolved_type
915 .extends
916 .parent_name()
917 .map(|p| p == existing_name.as_str())
918 .unwrap_or(false);
919 let existing_extends_current = existing_type
920 .extends
921 .parent_name()
922 .map(|p| p == defined_by.name())
923 .unwrap_or(false);
924
925 if existing_type.is_scale()
926 && (current_extends_existing || existing_extends_current)
927 {
928 if current_extends_existing {
929 unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
930 }
931 continue;
932 }
933
934 if existing_type.same_scale_family(resolved_type) {
935 continue;
936 }
937
938 return Err(Error::validation_with_context(
939 format!(
940 "Ambiguous unit '{}'. Defined in multiple types: '{}' and '{}'",
941 unit,
942 existing_name,
943 defined_by.name()
944 ),
945 Some(defined_by.source_location().clone()),
946 None::<String>,
947 Some(Arc::clone(spec)),
948 None,
949 ));
950 }
951 unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
952 }
953 Ok(())
954 }
955
956 fn add_ratio_units_to_index(
957 spec: &Arc<LemmaSpec>,
958 unit_index: &mut HashMap<String, (LemmaType, Option<TypeDef>)>,
959 resolved_type: &LemmaType,
960 defined_by: &TypeDef,
961 ) -> Result<(), Error> {
962 let units = Self::extract_units_from_type(&resolved_type.specifications);
963 for unit in units {
964 if let Some((existing_type, existing_def)) = unit_index.get(&unit) {
965 if existing_type.is_ratio() {
966 continue;
967 }
968 let existing_name: String = existing_def
969 .as_ref()
970 .map(|d| d.name().to_owned())
971 .unwrap_or_else(|| existing_type.name());
972 return Err(Error::validation_with_context(
973 format!(
974 "Ambiguous unit '{}'. Defined in multiple types: '{}' and '{}'",
975 unit,
976 existing_name,
977 defined_by.name()
978 ),
979 Some(defined_by.source_location().clone()),
980 None::<String>,
981 Some(Arc::clone(spec)),
982 None,
983 ));
984 }
985 unit_index.insert(unit, (resolved_type.clone(), Some(defined_by.clone())));
986 }
987 Ok(())
988 }
989
990 fn extract_units_from_type(specs: &TypeSpecification) -> Vec<String> {
991 match specs {
992 TypeSpecification::Scale { units, .. } => {
993 units.iter().map(|unit| unit.name.clone()).collect()
994 }
995 TypeSpecification::Ratio { units, .. } => {
996 units.iter().map(|unit| unit.name.clone()).collect()
997 }
998 _ => Vec::new(),
999 }
1000 }
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005 use super::*;
1006 use crate::engine::Context;
1007 use crate::parse;
1008 use crate::parsing::ast::{
1009 CommandArg, LemmaSpec, ParentType, PrimitiveKind, TypeConstraintCommand,
1010 };
1011 use crate::ResourceLimits;
1012 use rust_decimal::Decimal;
1013 use std::sync::Arc;
1014
1015 fn test_context_and_spec() -> (Context, Arc<LemmaSpec>) {
1016 let spec = LemmaSpec::new("test_spec".to_string());
1017 let arc = Arc::new(spec);
1018 let mut ctx = Context::new();
1019 ctx.insert_spec(Arc::clone(&arc), false)
1020 .expect("insert test spec");
1021 (ctx, arc)
1022 }
1023
1024 fn resolver_for_code(code: &str) -> (PerSliceTypeResolver<'static>, Vec<Arc<LemmaSpec>>) {
1025 let specs = parse(code, "test.lemma", &ResourceLimits::default())
1028 .unwrap()
1029 .specs;
1030 let ctx = Box::leak(Box::new(Context::new()));
1031 let mut spec_arcs = Vec::new();
1032 for spec in &specs {
1033 let arc = Arc::new(spec.clone());
1034 ctx.insert_spec(Arc::clone(&arc), spec.from_registry)
1035 .expect("insert spec");
1036 spec_arcs.push(arc);
1037 }
1038 let plan_hashes = Box::leak(Box::new(crate::planning::PlanHashRegistry::default()));
1039 let mut resolver = PerSliceTypeResolver::new(ctx, None, plan_hashes);
1040 for spec_arc in &spec_arcs {
1041 resolver.register_all(spec_arc);
1042 }
1043 (resolver, spec_arcs)
1044 }
1045
1046 fn resolver_single_spec(code: &str) -> (PerSliceTypeResolver<'static>, Arc<LemmaSpec>) {
1047 let (resolver, spec_arcs) = resolver_for_code(code);
1048 let spec_arc = spec_arcs.into_iter().next().expect("at least one spec");
1049 (resolver, spec_arc)
1050 }
1051
1052 #[test]
1053 fn test_registry_creation() {
1054 let (ctx, spec_arc) = test_context_and_spec();
1055 let ph = crate::planning::PlanHashRegistry::default();
1056 let resolver = PerSliceTypeResolver::new(&ctx, None, &ph);
1057 let resolved = resolver.resolve_named_types(&spec_arc).unwrap();
1058 assert!(resolved.named_types.is_empty());
1059 assert!(resolved.inline_type_definitions.is_empty());
1060 }
1061
1062 #[test]
1063 fn test_type_spec_for_primitive_covers_all_variants() {
1064 use crate::parsing::ast::PrimitiveKind;
1065 use crate::planning::semantics::type_spec_for_primitive;
1066
1067 for kind in [
1068 PrimitiveKind::Boolean,
1069 PrimitiveKind::Scale,
1070 PrimitiveKind::Number,
1071 PrimitiveKind::Percent,
1072 PrimitiveKind::Ratio,
1073 PrimitiveKind::Text,
1074 PrimitiveKind::Date,
1075 PrimitiveKind::Time,
1076 PrimitiveKind::Duration,
1077 ] {
1078 let spec = type_spec_for_primitive(kind);
1079 assert!(
1080 !matches!(
1081 spec,
1082 crate::planning::semantics::TypeSpecification::Undetermined
1083 ),
1084 "type_spec_for_primitive({:?}) returned Undetermined",
1085 kind
1086 );
1087 }
1088 }
1089
1090 #[test]
1091 fn test_register_named_type() {
1092 let (ctx, spec_arc) = test_context_and_spec();
1093 let ph = crate::planning::PlanHashRegistry::default();
1094 let mut resolver = PerSliceTypeResolver::new(&ctx, None, &ph);
1095 let type_def = TypeDef::Regular {
1096 source_location: crate::Source::new(
1097 "<test>",
1098 crate::parsing::ast::Span {
1099 start: 0,
1100 end: 0,
1101 line: 1,
1102 col: 0,
1103 },
1104 ),
1105 name: "money".to_string(),
1106 parent: ParentType::Primitive(PrimitiveKind::Number),
1107 constraints: None,
1108 };
1109
1110 let result = resolver.register_type(&spec_arc, type_def);
1111 assert!(result.is_ok());
1112 }
1113
1114 #[test]
1115 fn test_register_inline_type_definition() {
1116 use crate::parsing::ast::Reference;
1117 let (ctx, spec_arc) = test_context_and_spec();
1118 let ph = crate::planning::PlanHashRegistry::default();
1119 let mut resolver = PerSliceTypeResolver::new(&ctx, None, &ph);
1120 let fact_ref = Reference::local("age".to_string());
1121 let type_def = TypeDef::Inline {
1122 source_location: crate::Source::new(
1123 "<test>",
1124 crate::parsing::ast::Span {
1125 start: 0,
1126 end: 0,
1127 line: 1,
1128 col: 0,
1129 },
1130 ),
1131 parent: ParentType::Primitive(PrimitiveKind::Number),
1132 constraints: Some(vec![
1133 (
1134 TypeConstraintCommand::Minimum,
1135 vec![CommandArg::Number("0".to_string())],
1136 ),
1137 (
1138 TypeConstraintCommand::Maximum,
1139 vec![CommandArg::Number("150".to_string())],
1140 ),
1141 ]),
1142 fact_ref: fact_ref.clone(),
1143 from: None,
1144 };
1145
1146 let result = resolver.register_type(&spec_arc, type_def);
1147 assert!(result.is_ok());
1148 let resolved = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1149 assert!(resolved.inline_type_definitions.contains_key(&fact_ref));
1150 }
1151
1152 #[test]
1153 fn test_register_duplicate_type_fails() {
1154 let (ctx, spec_arc) = test_context_and_spec();
1155 let ph = crate::planning::PlanHashRegistry::default();
1156 let mut resolver = PerSliceTypeResolver::new(&ctx, None, &ph);
1157 let type_def = TypeDef::Regular {
1158 source_location: crate::Source::new(
1159 "<test>",
1160 crate::parsing::ast::Span {
1161 start: 0,
1162 end: 0,
1163 line: 1,
1164 col: 0,
1165 },
1166 ),
1167 name: "money".to_string(),
1168 parent: ParentType::Primitive(PrimitiveKind::Number),
1169 constraints: None,
1170 };
1171
1172 resolver.register_type(&spec_arc, type_def.clone()).unwrap();
1173 let result = resolver.register_type(&spec_arc, type_def);
1174 assert!(result.is_err());
1175 }
1176
1177 #[test]
1178 fn test_resolve_custom_type_from_primitive() {
1179 let (ctx, spec_arc) = test_context_and_spec();
1180 let ph = crate::planning::PlanHashRegistry::default();
1181 let mut resolver = PerSliceTypeResolver::new(&ctx, None, &ph);
1182 let type_def = TypeDef::Regular {
1183 source_location: crate::Source::new(
1184 "<test>",
1185 crate::parsing::ast::Span {
1186 start: 0,
1187 end: 0,
1188 line: 1,
1189 col: 0,
1190 },
1191 ),
1192 name: "money".to_string(),
1193 parent: ParentType::Primitive(PrimitiveKind::Number),
1194 constraints: None,
1195 };
1196
1197 resolver.register_type(&spec_arc, type_def).unwrap();
1198 let resolved = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1199
1200 assert!(resolved.named_types.contains_key("money"));
1201 let money_type = resolved.named_types.get("money").unwrap();
1202 assert_eq!(money_type.name, Some("money".to_string()));
1203 }
1204
1205 #[test]
1206 fn test_type_definition_resolution() {
1207 let (resolver, spec_arc) = resolver_single_spec(
1208 r#"spec test
1209type dice: number -> minimum 0 -> maximum 6"#,
1210 );
1211
1212 let resolved_types = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1213 let dice_type = resolved_types.named_types.get("dice").unwrap();
1214
1215 match &dice_type.specifications {
1216 TypeSpecification::Number {
1217 minimum, maximum, ..
1218 } => {
1219 assert_eq!(*minimum, Some(Decimal::from(0)));
1220 assert_eq!(*maximum, Some(Decimal::from(6)));
1221 }
1222 _ => panic!("Expected Number type specifications"),
1223 }
1224 }
1225
1226 #[test]
1227 fn test_type_definition_with_multiple_commands() {
1228 let (resolver, spec_arc) = resolver_single_spec(
1229 r#"spec test
1230type money: scale -> decimals 2 -> unit eur 1.0 -> unit usd 1.18"#,
1231 );
1232
1233 let resolved_types = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1234 let money_type = resolved_types.named_types.get("money").unwrap();
1235
1236 match &money_type.specifications {
1237 TypeSpecification::Scale {
1238 decimals, units, ..
1239 } => {
1240 assert_eq!(*decimals, Some(2));
1241 assert_eq!(units.len(), 2);
1242 assert!(units.iter().any(|u| u.name == "eur"));
1243 assert!(units.iter().any(|u| u.name == "usd"));
1244 }
1245 _ => panic!("Expected Scale type specifications"),
1246 }
1247 }
1248
1249 #[test]
1250 fn test_number_type_with_decimals() {
1251 let (resolver, spec_arc) = resolver_single_spec(
1252 r#"spec test
1253type price: number -> decimals 2 -> minimum 0"#,
1254 );
1255
1256 let resolved_types = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1257 let price_type = resolved_types.named_types.get("price").unwrap();
1258
1259 match &price_type.specifications {
1260 TypeSpecification::Number {
1261 decimals, minimum, ..
1262 } => {
1263 assert_eq!(*decimals, Some(2));
1264 assert_eq!(*minimum, Some(Decimal::from(0)));
1265 }
1266 _ => panic!("Expected Number type specifications with decimals"),
1267 }
1268 }
1269
1270 #[test]
1271 fn test_number_type_decimals_only() {
1272 let (resolver, spec_arc) = resolver_single_spec(
1273 r#"spec test
1274type precise_number: number -> decimals 4"#,
1275 );
1276
1277 let resolved_types = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1278 let precise_type = resolved_types.named_types.get("precise_number").unwrap();
1279
1280 match &precise_type.specifications {
1281 TypeSpecification::Number { decimals, .. } => {
1282 assert_eq!(*decimals, Some(4));
1283 }
1284 _ => panic!("Expected Number type with decimals 4"),
1285 }
1286 }
1287
1288 #[test]
1289 fn test_scale_type_decimals_only() {
1290 let (resolver, spec_arc) = resolver_single_spec(
1291 r#"spec test
1292type weight: scale -> unit kg 1 -> decimals 3"#,
1293 );
1294
1295 let resolved_types = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1296 let weight_type = resolved_types.named_types.get("weight").unwrap();
1297
1298 match &weight_type.specifications {
1299 TypeSpecification::Scale { decimals, .. } => {
1300 assert_eq!(*decimals, Some(3));
1301 }
1302 _ => panic!("Expected Scale type with decimals 3"),
1303 }
1304 }
1305
1306 #[test]
1307 fn test_ratio_type_accepts_optional_decimals_command() {
1308 let (resolver, spec_arc) = resolver_single_spec(
1309 r#"spec test
1310type ratio_type: ratio -> decimals 2"#,
1311 );
1312
1313 let resolved_types = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1314 let ratio_type = resolved_types.named_types.get("ratio_type").unwrap();
1315
1316 match &ratio_type.specifications {
1317 TypeSpecification::Ratio { decimals, .. } => {
1318 assert_eq!(
1319 *decimals,
1320 Some(2),
1321 "ratio type should accept decimals command"
1322 );
1323 }
1324 _ => panic!("Expected Ratio type with decimals 2"),
1325 }
1326 }
1327
1328 #[test]
1329 fn test_ratio_type_with_default_command() {
1330 let (resolver, spec_arc) = resolver_single_spec(
1331 r#"spec test
1332type percentage: ratio -> minimum 0 -> maximum 1 -> default 0.5"#,
1333 );
1334
1335 let resolved_types = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1336 let percentage_type = resolved_types.named_types.get("percentage").unwrap();
1337
1338 match &percentage_type.specifications {
1339 TypeSpecification::Ratio {
1340 minimum,
1341 maximum,
1342 default,
1343 ..
1344 } => {
1345 assert_eq!(
1346 *minimum,
1347 Some(Decimal::from(0)),
1348 "ratio type should have minimum 0"
1349 );
1350 assert_eq!(
1351 *maximum,
1352 Some(Decimal::from(1)),
1353 "ratio type should have maximum 1"
1354 );
1355 assert_eq!(
1356 *default,
1357 Some(Decimal::from_i128_with_scale(5, 1)),
1358 "ratio type with default command must work"
1359 );
1360 }
1361 _ => panic!("Expected Ratio type with minimum, maximum, and default"),
1362 }
1363 }
1364
1365 #[test]
1366 fn test_scale_extension_chain_same_family_units_allowed() {
1367 let (resolver, spec_arc) = resolver_single_spec(
1368 r#"spec test
1369type money: scale -> unit eur 1
1370type money2: money -> unit usd 1.24"#,
1371 );
1372
1373 let result = resolver.resolve_types_internal(&spec_arc, true);
1374 assert!(
1375 result.is_ok(),
1376 "Scale extension chain should resolve: {:?}",
1377 result.err()
1378 );
1379
1380 let resolved = result.unwrap();
1381 assert!(
1382 resolved.unit_index.contains_key("eur"),
1383 "eur should be in unit_index"
1384 );
1385 assert!(
1386 resolved.unit_index.contains_key("usd"),
1387 "usd should be in unit_index"
1388 );
1389 let (eur_type, _) = resolved.unit_index.get("eur").unwrap();
1390 let (usd_type, _) = resolved.unit_index.get("usd").unwrap();
1391 assert_eq!(
1392 eur_type.name.as_deref(),
1393 Some("money2"),
1394 "more derived type (money2) should own eur for conversion"
1395 );
1396 assert_eq!(usd_type.name.as_deref(), Some("money2"));
1397 }
1398
1399 #[test]
1400 fn test_invalid_parent_type_in_named_type_should_error() {
1401 let (resolver, spec_arc) = resolver_single_spec(
1402 r#"spec test
1403type invalid: nonexistent_type -> minimum 0"#,
1404 );
1405
1406 let result = resolver.resolve_types_internal(&spec_arc, true);
1407 assert!(result.is_err(), "Should reject invalid parent type");
1408
1409 let errs = result.unwrap_err();
1410 assert!(!errs.is_empty(), "expected at least one error");
1411 let error_msg = errs[0].to_string();
1412 assert!(
1413 error_msg.contains("Unknown type") && error_msg.contains("nonexistent_type"),
1414 "Error should mention unknown type. Got: {}",
1415 error_msg
1416 );
1417 }
1418
1419 #[test]
1420 fn test_invalid_primitive_type_name_should_error() {
1421 let (resolver, spec_arc) = resolver_single_spec(
1422 r#"spec test
1423type invalid: choice -> option "a""#,
1424 );
1425
1426 let result = resolver.resolve_types_internal(&spec_arc, true);
1427 assert!(result.is_err(), "Should reject invalid type base 'choice'");
1428
1429 let errs = result.unwrap_err();
1430 assert!(!errs.is_empty(), "expected at least one error");
1431 let error_msg = errs[0].to_string();
1432 assert!(
1433 error_msg.contains("Unknown type") && error_msg.contains("choice"),
1434 "Error should mention unknown type 'choice'. Got: {}",
1435 error_msg
1436 );
1437 }
1438
1439 #[test]
1440 fn test_unit_constraint_validation_errors_are_reported() {
1441 let (resolver, spec_arc) = resolver_single_spec(
1442 r#"spec test
1443type money: scale
1444 -> unit eur 1.00
1445 -> unit usd 1.19
1446
1447type money2: money
1448 -> unit eur 1.20
1449 -> unit usd 1.21
1450 -> unit gbp 1.30"#,
1451 );
1452
1453 let result = resolver.resolve_types_internal(&spec_arc, true);
1454 assert!(
1455 result.is_err(),
1456 "Expected unit constraint conflicts to error"
1457 );
1458
1459 let errs = result.unwrap_err();
1460 assert!(!errs.is_empty(), "expected at least one error");
1461 let error_msg = errs
1462 .iter()
1463 .map(ToString::to_string)
1464 .collect::<Vec<_>>()
1465 .join("; ");
1466 assert!(
1467 error_msg.contains("eur") || error_msg.contains("usd"),
1468 "Error should mention the conflicting units. Got: {}",
1469 error_msg
1470 );
1471 }
1472
1473 #[test]
1474 fn test_spec_level_unit_ambiguity_errors_are_reported() {
1475 let (resolver, spec_arc) = resolver_single_spec(
1476 r#"spec test
1477type money_a: scale
1478 -> unit eur 1.00
1479 -> unit usd 1.19
1480
1481type money_b: scale
1482 -> unit eur 1.00
1483 -> unit usd 1.20
1484
1485type length_a: scale
1486 -> unit meter 1.0
1487
1488type length_b: scale
1489 -> unit meter 1.0"#,
1490 );
1491
1492 let result = resolver.resolve_types_internal(&spec_arc, true);
1493 assert!(
1494 result.is_err(),
1495 "Expected ambiguous unit definitions to error"
1496 );
1497
1498 let errs = result.unwrap_err();
1499 assert!(!errs.is_empty(), "expected at least one error");
1500 let error_msg = errs
1501 .iter()
1502 .map(ToString::to_string)
1503 .collect::<Vec<_>>()
1504 .join("; ");
1505 assert!(
1506 error_msg.contains("eur") || error_msg.contains("usd") || error_msg.contains("meter"),
1507 "Error should mention at least one ambiguous unit. Got: {}",
1508 error_msg
1509 );
1510 }
1511
1512 #[test]
1513 fn test_number_type_cannot_have_units() {
1514 let (resolver, spec_arc) = resolver_single_spec(
1515 r#"spec test
1516type price: number
1517 -> unit eur 1.00"#,
1518 );
1519
1520 let result = resolver.resolve_types_internal(&spec_arc, true);
1521 assert!(result.is_err(), "Number types must reject unit commands");
1522
1523 let errs = result.unwrap_err();
1524 assert!(!errs.is_empty(), "expected at least one error");
1525 let error_msg = errs[0].to_string();
1526 assert!(
1527 error_msg.contains("unit") && error_msg.contains("number"),
1528 "Error should mention units are invalid on number. Got: {}",
1529 error_msg
1530 );
1531 }
1532
1533 #[test]
1534 fn test_scale_type_can_have_units() {
1535 let (resolver, spec_arc) = resolver_single_spec(
1536 r#"spec test
1537type money: scale
1538 -> unit eur 1.00
1539 -> unit usd 1.19"#,
1540 );
1541
1542 let resolved = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1543 let money_type = resolved.named_types.get("money").unwrap();
1544
1545 match &money_type.specifications {
1546 TypeSpecification::Scale { units, .. } => {
1547 assert_eq!(units.len(), 2);
1548 assert!(units.iter().any(|u| u.name == "eur"));
1549 assert!(units.iter().any(|u| u.name == "usd"));
1550 }
1551 other => panic!("Expected Scale type specifications, got {:?}", other),
1552 }
1553 }
1554
1555 #[test]
1556 fn test_extending_type_inherits_units() {
1557 let (resolver, spec_arc) = resolver_single_spec(
1558 r#"spec test
1559type money: scale
1560 -> unit eur 1.00
1561 -> unit usd 1.19
1562
1563type my_money: money
1564 -> unit gbp 1.30"#,
1565 );
1566
1567 let resolved = resolver.resolve_types_internal(&spec_arc, true).unwrap();
1568 let my_money_type = resolved.named_types.get("my_money").unwrap();
1569
1570 match &my_money_type.specifications {
1571 TypeSpecification::Scale { units, .. } => {
1572 assert_eq!(units.len(), 3);
1573 assert!(units.iter().any(|u| u.name == "eur"));
1574 assert!(units.iter().any(|u| u.name == "usd"));
1575 assert!(units.iter().any(|u| u.name == "gbp"));
1576 }
1577 other => panic!("Expected Scale type specifications, got {:?}", other),
1578 }
1579 }
1580
1581 #[test]
1582 fn test_duplicate_unit_in_same_type_is_rejected() {
1583 let (resolver, spec_arc) = resolver_single_spec(
1584 r#"spec test
1585type money: scale
1586 -> unit eur 1.00
1587 -> unit eur 1.19"#,
1588 );
1589
1590 let result = resolver.resolve_types_internal(&spec_arc, true);
1591 assert!(
1592 result.is_err(),
1593 "Duplicate units within a type should error"
1594 );
1595
1596 let errs = result.unwrap_err();
1597 assert!(!errs.is_empty(), "expected at least one error");
1598 let error_msg = errs[0].to_string();
1599 assert!(
1600 error_msg.contains("Duplicate unit")
1601 || error_msg.contains("duplicate")
1602 || error_msg.contains("already exists")
1603 || error_msg.contains("eur"),
1604 "Error should mention duplicate unit issue. Got: {}",
1605 error_msg
1606 );
1607 }
1608
1609 #[test]
1610 fn repro_named_type_source_location_panic() {
1611 use crate::parsing::ast::{CommandArg, ParentType, PrimitiveKind};
1612 let code = r#"spec nettoloon
1613type geld: scale
1614 -> decimals 2
1615 -> unit eur 1.00
1616 -> minimum 0 eur
1617fact bruto_salaris: 0 eur"#;
1618 let (mut resolver, spec_arc) = resolver_single_spec(code);
1619 let fact_ref = Reference::local("bruto_salaris".to_string());
1620 let inline_def = TypeDef::Inline {
1621 source_location: spec_arc.types[0].source_location().clone(),
1622 parent: ParentType::Primitive(PrimitiveKind::Scale),
1623 constraints: Some(vec![(
1624 TypeConstraintCommand::Unit,
1625 vec![
1626 CommandArg::Label("eur".to_string()),
1627 CommandArg::Number("1.00".to_string()),
1628 ],
1629 )]),
1630 fact_ref: fact_ref.clone(),
1631 from: None,
1632 };
1633 resolver.register_type(&spec_arc, inline_def).unwrap();
1634 let _ = resolver.resolve_types_internal(&spec_arc, true);
1635 }
1636}