1use crate::planning::semantics::{
6 primitive_ratio_arc, LemmaType, SpecVersionKey, TypeFamily, TypeSpecification,
7};
8use indexmap::IndexMap;
9use std::collections::{BTreeMap, BTreeSet, HashMap};
10use std::sync::Arc;
11
12#[derive(Debug, Clone)]
15pub enum PlanningOwner {
16 PrimitiveRatio,
17 Local {
18 type_name: String,
19 },
20 Imported {
21 import_path: Vec<String>,
22 type_name: String,
23 lemma_type: Arc<LemmaType>,
24 },
25}
26
27impl PlanningOwner {
28 fn type_name(&self) -> &str {
29 match self {
30 Self::PrimitiveRatio => "ratio",
31 Self::Local { type_name } | Self::Imported { type_name, .. } => type_name,
32 }
33 }
34
35 fn import_path(&self) -> &[String] {
36 match self {
37 Self::PrimitiveRatio | Self::Local { .. } => &[],
38 Self::Imported { import_path, .. } => import_path.as_slice(),
39 }
40 }
41
42 fn same_slot(&self, type_name: &str, import_path: &[String]) -> bool {
43 match self {
44 Self::PrimitiveRatio => false,
45 Self::Local {
46 type_name: existing,
47 } => import_path.is_empty() && existing == type_name,
48 Self::Imported {
49 import_path: existing_path,
50 type_name: existing,
51 ..
52 } => existing_path.as_slice() == import_path && existing == type_name,
53 }
54 }
55
56 pub fn lemma_type<'a>(
58 &'a self,
59 resolved: &'a IndexMap<String, Arc<LemmaType>>,
60 ) -> &'a LemmaType {
61 match self {
62 Self::PrimitiveRatio => primitive_ratio_arc().as_ref(),
63 Self::Local { type_name } => resolved
64 .get(type_name)
65 .expect("BUG: local unit owner missing from resolved")
66 .as_ref(),
67 Self::Imported { lemma_type, .. } => lemma_type.as_ref(),
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
74pub enum UnitOwner {
75 PrimitiveRatio,
76 Named {
77 import_path: Vec<String>,
78 type_name: String,
79 lemma_type: Arc<LemmaType>,
80 },
81}
82
83impl UnitOwner {
84 #[must_use]
85 pub fn lemma_type(&self) -> &Arc<LemmaType> {
86 match self {
87 Self::PrimitiveRatio => primitive_ratio_arc(),
88 Self::Named { lemma_type, .. } => lemma_type,
89 }
90 }
91
92 #[must_use]
93 pub fn type_name(&self) -> &str {
94 match self {
95 Self::PrimitiveRatio => "ratio",
96 Self::Named { type_name, .. } => type_name,
97 }
98 }
99
100 #[must_use]
101 pub fn import_path(&self) -> &[String] {
102 match self {
103 Self::PrimitiveRatio => &[],
104 Self::Named { import_path, .. } => import_path.as_slice(),
105 }
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum UnitMergeConflict {
112 ConflictingFactors { unit: String, family: String },
113}
114
115#[derive(Debug, Clone, Default)]
117pub struct PlanningUnitIndex {
118 by_bare: IndexMap<String, Vec<PlanningOwner>>,
119}
120
121impl PlanningUnitIndex {
122 #[must_use]
123 pub fn new() -> Self {
124 Self::default()
125 }
126
127 #[must_use]
128 pub fn has_unique_owner(&self, bare: &str) -> bool {
129 matches!(self.owners_for(bare), [_])
130 }
131
132 #[must_use]
133 pub fn owners_for(&self, bare: &str) -> &[PlanningOwner] {
134 self.by_bare.get(bare).map(Vec::as_slice).unwrap_or(&[])
135 }
136
137 pub fn iter_owners(&self) -> impl Iterator<Item = (&str, &PlanningOwner)> {
138 self.by_bare
139 .iter()
140 .flat_map(|(bare, owners)| owners.iter().map(move |owner| (bare.as_str(), owner)))
141 }
142
143 pub fn insert_owner(&mut self, bare: String, owner: PlanningOwner) {
144 let owners = self.by_bare.entry(bare).or_default();
145 if let Some(existing) = owners
146 .iter_mut()
147 .find(|existing| planning_owners_same_slot(existing, &owner))
148 {
149 *existing = owner;
150 return;
151 }
152 owners.push(owner);
153 }
154
155 pub fn merge_measure_unit(
156 &mut self,
157 unit: String,
158 resolved_type: &Arc<LemmaType>,
159 type_name: &str,
160 import_path: &[String],
161 measure_family: &str,
162 locals: &HashMap<String, Arc<LemmaType>>,
163 ) -> Result<(), UnitMergeConflict> {
164 let owners = self.by_bare.entry(unit.clone()).or_default();
165 if owners
166 .iter()
167 .any(|owner| owner.same_slot(type_name, import_path))
168 {
169 return Ok(());
170 }
171
172 let resolved_ref = resolved_type.as_ref();
173 let mut skip_insert = false;
174 let mut reclaim_indices = Vec::new();
175 for (index, owner) in owners.iter().enumerate() {
176 let existing_type = planning_owner_type(owner, locals);
177 let existing_name = owner.type_name();
178 let existing_path = owner.import_path();
179 let parent_ref = resolved_ref.extends.parent_name();
180 let current_extends_existing = parent_ref
181 .is_some_and(|parent| parent_matches_owner(parent, existing_path, existing_name));
182 let existing_extends_current = existing_type
183 .extends
184 .parent_name()
185 .is_some_and(|parent| parent_matches_owner(parent, import_path, type_name));
186
187 if existing_type.is_measure() && (current_extends_existing || existing_extends_current)
188 {
189 if current_extends_existing {
190 skip_insert = true;
191 } else {
192 reclaim_indices.push(index);
193 }
194 continue;
195 }
196
197 if existing_type.is_measure() && existing_type.same_measure_family(resolved_ref) {
198 if let (
199 TypeSpecification::Measure {
200 units: existing_units,
201 ..
202 },
203 TypeSpecification::Measure {
204 units: new_units, ..
205 },
206 ) = (&existing_type.specifications, &resolved_ref.specifications)
207 {
208 let same_factor = existing_units
209 .iter()
210 .find(|existing_unit| existing_unit.name == unit)
211 .zip(new_units.iter().find(|new_unit| new_unit.name == unit))
212 .is_some_and(|(existing_unit, new_unit)| {
213 existing_unit.same_consumer_scale(new_unit, existing_path, import_path)
214 });
215 if same_factor {
216 if import_path.is_empty() && existing_path.is_empty() {
219 skip_insert = true;
220 }
221 continue;
222 }
223 return Err(UnitMergeConflict::ConflictingFactors {
224 unit,
225 family: measure_family.to_string(),
226 });
227 }
228 }
229 }
230
231 for index in reclaim_indices.into_iter().rev() {
232 owners.remove(index);
233 }
234
235 if !skip_insert {
236 owners.push(planning_named_owner(type_name, import_path, resolved_type));
237 }
238 Ok(())
239 }
240
241 pub fn merge_ratio_unit(
242 &mut self,
243 unit: String,
244 resolved_type: &Arc<LemmaType>,
245 type_name: &str,
246 import_path: &[String],
247 locals: &HashMap<String, Arc<LemmaType>>,
248 ) -> Result<(), UnitMergeConflict> {
249 let owners = self.by_bare.entry(unit.clone()).or_default();
250 if let Some(existing) = owners
251 .iter_mut()
252 .find(|owner| owner.same_slot(type_name, import_path))
253 {
254 if let PlanningOwner::Imported { lemma_type, .. } = existing {
255 *lemma_type = Arc::clone(resolved_type);
256 }
257 return Ok(());
258 }
259
260 if owners.is_empty() {
261 owners.push(planning_named_owner(type_name, import_path, resolved_type));
262 return Ok(());
263 }
264
265 let resolved_ref = resolved_type.as_ref();
267 let mut skip_insert = false;
268 for owner in owners.iter() {
269 if matches!(owner, PlanningOwner::PrimitiveRatio) {
270 continue;
271 }
272 let existing_type = planning_owner_type(owner, locals);
273 if !existing_type.is_ratio() || existing_type.name() == resolved_ref.name() {
274 continue;
275 }
276 if matches!(unit.as_str(), "percent" | "permille") {
277 if let (
278 TypeSpecification::Ratio {
279 units: existing_units,
280 ..
281 },
282 TypeSpecification::Ratio {
283 units: new_units, ..
284 },
285 ) = (&existing_type.specifications, &resolved_ref.specifications)
286 {
287 let same_factor = existing_units
288 .iter()
289 .find(|existing_unit| existing_unit.name == unit)
290 .zip(new_units.iter().find(|new_unit| new_unit.name == unit))
291 .is_some_and(|(existing_unit, new_unit)| {
292 existing_unit.value == new_unit.value
293 });
294 if same_factor && import_path.is_empty() && owner.import_path().is_empty() {
295 skip_insert = true;
296 }
297 }
298 }
299 }
300 if !skip_insert {
301 owners.push(planning_named_owner(type_name, import_path, resolved_type));
302 }
303 Ok(())
304 }
305
306 pub fn resolve_owner(
307 &self,
308 unit_ref: &str,
309 resolved: &IndexMap<String, Arc<LemmaType>>,
310 ) -> Result<(String, &PlanningOwner), String> {
311 let bare_key = unit_ref_bare(unit_ref);
312 let owners = self.owners_for(&bare_key);
313 resolve_owners_for_ref(
314 owners,
315 unit_ref,
316 |owner| owner.type_name(),
317 |owner| owner.import_path(),
318 |owner| planning_owner_lemma_arc(owner, resolved),
319 )
320 }
321
322 pub fn resolve_with_named_types(
323 &self,
324 unit_ref: &str,
325 resolved: &IndexMap<String, Arc<LemmaType>>,
326 ) -> Result<(String, Arc<LemmaType>), String> {
327 match self.resolve_owner(unit_ref, resolved) {
328 Ok((bare, owner)) => Ok((
329 bare,
330 Arc::clone(match owner {
331 PlanningOwner::PrimitiveRatio => primitive_ratio_arc(),
332 PlanningOwner::Local { type_name } => resolved
333 .get(type_name)
334 .expect("BUG: local unit owner missing from resolved"),
335 PlanningOwner::Imported { lemma_type, .. } => lemma_type,
336 }),
337 )),
338 Err(index_err) => {
339 UnitIndex::resolve_via_named_measure_type(unit_ref, resolved).ok_or(index_err)
340 }
341 }
342 }
343
344 pub fn into_iter_owners(self) -> impl Iterator<Item = (String, PlanningOwner)> {
345 self.by_bare
346 .into_iter()
347 .flat_map(|(bare, owners)| owners.into_iter().map(move |owner| (bare.clone(), owner)))
348 }
349
350 #[must_use]
352 pub(crate) fn bind(
353 self,
354 resolved: &IndexMap<String, Arc<LemmaType>>,
355 this_spec: &SpecVersionKey,
356 ) -> UnitIndex {
357 let mut bound = UnitIndex::new();
358 for (unit_name, owner) in self.into_iter_owners() {
359 let bound_owner = match owner {
360 PlanningOwner::PrimitiveRatio => UnitOwner::PrimitiveRatio,
361 PlanningOwner::Local { type_name } => {
362 let lemma_type = Arc::clone(
363 resolved
364 .get(&type_name)
365 .expect("BUG: local unit owner missing from resolved"),
366 );
367 if !type_declares_unit(lemma_type.as_ref(), &unit_name) {
368 continue;
369 }
370 UnitOwner::Named {
371 import_path: Vec::new(),
372 type_name,
373 lemma_type,
374 }
375 }
376 PlanningOwner::Imported {
377 import_path,
378 type_name,
379 lemma_type,
380 } => {
381 if !type_declares_unit(lemma_type.as_ref(), &unit_name) {
382 continue;
383 }
384 UnitOwner::Named {
385 import_path,
386 type_name,
387 lemma_type,
388 }
389 }
390 };
391 bound.insert_owner(unit_name, bound_owner);
392 }
393 for (type_name, lemma_type) in resolved {
394 let TypeSpecification::Measure { units, .. } = &lemma_type.specifications else {
395 continue;
396 };
397 let is_family_root = lemma_type.measure_family().is_some_and(|family| {
398 family.is_declared_in(this_spec) && family.name == *type_name
399 });
400 if !is_family_root {
401 continue;
402 }
403 for unit in units.iter() {
404 let already = bound
405 .owners_for(&unit.name)
406 .iter()
407 .any(|owner| match owner {
408 UnitOwner::Named {
409 type_name: owner_name,
410 import_path,
411 ..
412 } if import_path.is_empty() && owner_name == type_name => true,
413 UnitOwner::Named {
414 lemma_type: owner_type,
415 ..
416 } => owner_type.same_measure_family(lemma_type.as_ref()),
417 UnitOwner::PrimitiveRatio => false,
418 });
419 if !already {
420 bound.insert_owner(
421 unit.name.clone(),
422 UnitOwner::Named {
423 import_path: Vec::new(),
424 type_name: type_name.clone(),
425 lemma_type: Arc::clone(lemma_type),
426 },
427 );
428 }
429 }
430 }
431 bound
432 }
433}
434
435#[derive(Debug, Clone, Default)]
437pub struct UnitIndex {
438 by_bare: IndexMap<String, Vec<UnitOwner>>,
439 distinct_types: IndexMap<(Vec<String>, String), Arc<LemmaType>>,
441 family_roots: BTreeMap<TypeFamily, Arc<LemmaType>>,
443}
444
445impl UnitIndex {
446 #[must_use]
447 pub fn new() -> Self {
448 Self::default()
449 }
450
451 #[must_use]
452 pub fn unique_owner(&self, bare: &str) -> Option<&Arc<LemmaType>> {
453 let owners = self.by_bare.get(bare)?;
454 match owners.as_slice() {
455 [only] => Some(only.lemma_type()),
456 _ => None,
457 }
458 }
459
460 #[must_use]
461 pub fn owners_for(&self, bare: &str) -> &[UnitOwner] {
462 self.by_bare.get(bare).map(Vec::as_slice).unwrap_or(&[])
463 }
464
465 pub fn iter_entries(&self) -> impl Iterator<Item = (&str, &Arc<LemmaType>)> {
466 self.by_bare.iter().flat_map(|(bare, owners)| {
467 owners
468 .iter()
469 .map(move |owner| (bare.as_str(), owner.lemma_type()))
470 })
471 }
472
473 pub fn iter_owners(&self) -> impl Iterator<Item = (&str, &UnitOwner)> {
474 self.by_bare
475 .iter()
476 .flat_map(|(bare, owners)| owners.iter().map(move |owner| (bare.as_str(), owner)))
477 }
478
479 pub fn values(&self) -> impl Iterator<Item = &Arc<LemmaType>> {
480 self.distinct_types.values()
481 }
482
483 #[must_use]
485 pub fn family_root(&self, family: &TypeFamily) -> Option<&Arc<LemmaType>> {
486 self.family_roots.get(family)
487 }
488
489 pub fn keys(&self) -> impl Iterator<Item = &String> {
490 self.by_bare.keys()
491 }
492
493 pub fn insert_owner(&mut self, bare: String, owner: UnitOwner) {
494 let slot = owner_slot(&owner);
495 let lemma_type = Arc::clone(owner.lemma_type());
496 let owners = self.by_bare.entry(bare).or_default();
497 if let Some(existing) = owners
498 .iter_mut()
499 .find(|existing| bound_owners_same_slot(existing, &owner))
500 {
501 *existing = owner;
502 self.refresh_distinct_slot(slot, lemma_type);
503 return;
504 }
505 owners.push(owner);
506 if let indexmap::map::Entry::Vacant(entry) = self.distinct_types.entry(slot) {
507 let stored = entry.insert(lemma_type);
508 record_family_root(&mut self.family_roots, stored);
509 }
510 }
511
512 fn refresh_distinct_slot(&mut self, slot: (Vec<String>, String), lemma_type: Arc<LemmaType>) {
513 let previous = self
514 .distinct_types
515 .insert(slot, Arc::clone(&lemma_type))
516 .expect("BUG: same-slot replace requires an existing distinct owner");
517 if let Some(previous_family) = previous.family() {
518 if previous.name.as_deref() == Some(previous_family.name.as_str()) {
519 if let Some(root) = self.family_roots.get_mut(previous_family) {
520 if Arc::ptr_eq(root, &previous) {
521 *root = Arc::clone(&lemma_type);
522 }
523 }
524 }
525 }
526 record_family_root(&mut self.family_roots, &lemma_type);
527 }
528
529 pub fn resolve(&self, unit_ref: &str) -> Result<(String, Arc<LemmaType>), String> {
530 self.resolve_owner(unit_ref)
531 .map(|(bare, owner)| (bare, Arc::clone(owner.lemma_type())))
532 }
533
534 pub fn resolve_owner(&self, unit_ref: &str) -> Result<(String, &UnitOwner), String> {
535 resolve_owners_for_ref(
536 self.owners_for(&unit_ref_bare(unit_ref)),
537 unit_ref,
538 |owner| owner.type_name(),
539 |owner| owner.import_path(),
540 |owner| Arc::clone(owner.lemma_type()),
541 )
542 }
543
544 #[must_use]
545 pub fn resolve_via_named_measure_type(
546 unit_ref: &str,
547 resolved: &IndexMap<String, Arc<LemmaType>>,
548 ) -> Option<(String, Arc<LemmaType>)> {
549 let segments: Vec<String> = unit_ref
550 .split('.')
551 .map(|segment| crate::parsing::ast::ascii_lowercase_logical_name(segment.to_string()))
552 .filter(|segment| !segment.is_empty())
553 .collect();
554 let (type_name, bare) = match segments.as_slice() {
555 [type_name, bare] => (type_name.as_str(), bare.as_str()),
556 _ => return None,
557 };
558 let lemma_type = resolved.get(type_name)?;
559 if !lemma_type.is_measure() {
560 return None;
561 }
562 let names = lemma_type.measure_unit_names()?;
563 if !names.contains(&bare) {
564 return None;
565 }
566 Some((bare.to_string(), Arc::clone(lemma_type)))
567 }
568
569 pub fn resolve_with_named_types(
570 &self,
571 unit_ref: &str,
572 resolved: &IndexMap<String, Arc<LemmaType>>,
573 ) -> Result<(String, Arc<LemmaType>), String> {
574 match self.resolve(unit_ref) {
575 Ok(hit) => Ok(hit),
576 Err(index_err) => {
577 Self::resolve_via_named_measure_type(unit_ref, resolved).ok_or(index_err)
578 }
579 }
580 }
581
582 #[must_use]
583 pub fn owning_type_for_signature_factor<'a>(
584 &'a self,
585 unit_name: &str,
586 typed_owners: &[&'a LemmaType],
587 ) -> Option<&'a LemmaType> {
588 for typed in typed_owners {
589 if type_declares_unit(typed, unit_name) {
590 return Some(typed);
591 }
592 }
593 match self.owners_for(unit_name) {
594 [only] => Some(only.lemma_type().as_ref()),
595 _ => None,
596 }
597 }
598}
599
600fn planning_named_owner(
601 type_name: &str,
602 import_path: &[String],
603 resolved_type: &Arc<LemmaType>,
604) -> PlanningOwner {
605 if import_path.is_empty() {
606 PlanningOwner::Local {
607 type_name: type_name.to_string(),
608 }
609 } else {
610 PlanningOwner::Imported {
611 import_path: import_path.to_vec(),
612 type_name: type_name.to_string(),
613 lemma_type: Arc::clone(resolved_type),
614 }
615 }
616}
617
618fn parent_matches_owner(parent: &str, owner_path: &[String], owner_type_name: &str) -> bool {
619 if owner_path.is_empty() {
620 parent == owner_type_name
621 } else {
622 let mut segments = owner_path.to_vec();
623 segments.push(owner_type_name.to_string());
624 segments.join(".") == parent
625 }
626}
627
628fn planning_owner_type<'a>(
629 owner: &'a PlanningOwner,
630 locals: &'a HashMap<String, Arc<LemmaType>>,
631) -> &'a LemmaType {
632 match owner {
633 PlanningOwner::PrimitiveRatio => primitive_ratio_arc().as_ref(),
634 PlanningOwner::Local { type_name } => locals
635 .get(type_name)
636 .expect("BUG: local unit owner missing from resolved")
637 .as_ref(),
638 PlanningOwner::Imported { lemma_type, .. } => lemma_type.as_ref(),
639 }
640}
641
642fn planning_owner_lemma_arc(
643 owner: &PlanningOwner,
644 resolved: &IndexMap<String, Arc<LemmaType>>,
645) -> Arc<LemmaType> {
646 match owner {
647 PlanningOwner::PrimitiveRatio => Arc::clone(primitive_ratio_arc()),
648 PlanningOwner::Local { type_name } => Arc::clone(
649 resolved
650 .get(type_name)
651 .expect("BUG: local unit owner missing from resolved"),
652 ),
653 PlanningOwner::Imported { lemma_type, .. } => Arc::clone(lemma_type),
654 }
655}
656
657fn type_declares_unit(lemma_type: &LemmaType, unit_name: &str) -> bool {
658 match &lemma_type.specifications {
659 TypeSpecification::Measure { units, .. } => units.get(unit_name).is_ok(),
660 TypeSpecification::Ratio { units, .. } => units.get(unit_name).is_ok(),
661 _ => false,
662 }
663}
664
665fn planning_owners_same_slot(left: &PlanningOwner, right: &PlanningOwner) -> bool {
666 match (left, right) {
667 (PlanningOwner::PrimitiveRatio, PlanningOwner::PrimitiveRatio) => true,
668 (PlanningOwner::Local { type_name: a }, PlanningOwner::Local { type_name: b }) => a == b,
669 (
670 PlanningOwner::Imported {
671 import_path: path_a,
672 type_name: name_a,
673 ..
674 },
675 PlanningOwner::Imported {
676 import_path: path_b,
677 type_name: name_b,
678 ..
679 },
680 ) => path_a == path_b && name_a == name_b,
681 _ => false,
682 }
683}
684
685fn bound_owners_same_slot(left: &UnitOwner, right: &UnitOwner) -> bool {
686 match (left, right) {
687 (UnitOwner::PrimitiveRatio, UnitOwner::PrimitiveRatio) => true,
688 (
689 UnitOwner::Named {
690 type_name: name_a,
691 import_path: path_a,
692 ..
693 },
694 UnitOwner::Named {
695 type_name: name_b,
696 import_path: path_b,
697 ..
698 },
699 ) => name_a == name_b && path_a == path_b,
700 _ => false,
701 }
702}
703
704fn owner_slot(owner: &UnitOwner) -> (Vec<String>, String) {
705 (owner.import_path().to_vec(), owner.type_name().to_string())
706}
707
708fn record_family_root(
709 family_roots: &mut BTreeMap<TypeFamily, Arc<LemmaType>>,
710 lemma_type: &Arc<LemmaType>,
711) {
712 let Some(family) = lemma_type.family() else {
713 return;
714 };
715 if lemma_type.name.as_deref() != Some(family.name.as_str()) {
716 return;
717 }
718 family_roots
719 .entry(family.clone())
720 .or_insert_with(|| Arc::clone(lemma_type));
721}
722
723fn unit_ref_bare(unit_ref: &str) -> String {
724 unit_ref
725 .split('.')
726 .map(|segment| crate::parsing::ast::ascii_lowercase_logical_name(segment.to_string()))
727 .rfind(|segment| !segment.is_empty())
728 .unwrap_or_default()
729}
730
731fn unit_scalar_for_bare(lemma_type: &LemmaType, bare: &str) -> Option<UnitScalar> {
732 match &lemma_type.specifications {
733 TypeSpecification::Measure { units, .. } => units
734 .get(bare)
735 .ok()
736 .and_then(|unit| unit.literal_factor().cloned().map(UnitScalar::Measure)),
737 TypeSpecification::Ratio { units, .. } => units
738 .get(bare)
739 .ok()
740 .map(|unit| UnitScalar::Ratio(unit.value.clone())),
741 _ => None,
742 }
743}
744
745#[derive(Debug, Clone, PartialEq, Eq)]
746enum UnitScalar {
747 Measure(crate::computation::rational::RationalInteger),
748 Ratio(crate::computation::rational::RationalInteger),
749}
750
751fn try_collapse_equivalent_owners<'a, O>(
752 candidates: &[&'a O],
753 bare: &str,
754 lemma_type: &impl Fn(&O) -> Arc<LemmaType>,
755) -> Result<&'a O, ()> {
756 let first = *candidates
757 .first()
758 .expect("BUG: collapse requires non-empty candidates");
759 let first_type = lemma_type(first);
760 if !type_declares_unit(first_type.as_ref(), bare) {
761 return Err(());
762 }
763 let first_scalar = unit_scalar_for_bare(first_type.as_ref(), bare).ok_or(())?;
764 for owner in candidates.iter().copied().skip(1) {
765 let other_type = lemma_type(owner);
766 let same_root = first_type.as_ref() == other_type.as_ref()
767 || first_type.same_measure_family(other_type.as_ref())
768 || (first_type.is_ratio()
769 && other_type.is_ratio()
770 && first_type.ratio_family().is_some()
771 && first_type.ratio_family() == other_type.ratio_family());
772 if !same_root {
773 return Err(());
774 }
775 if unit_scalar_for_bare(other_type.as_ref(), bare) != Some(first_scalar.clone()) {
776 return Err(());
777 }
778 }
779 Ok(first)
780}
781
782fn resolve_owners_for_ref<'a, O>(
783 owners: &'a [O],
784 unit_ref: &str,
785 type_name: impl Fn(&O) -> &str + Copy,
786 import_path: impl Fn(&O) -> &[String] + Copy,
787 lemma_type: impl Fn(&O) -> Arc<LemmaType>,
788) -> Result<(String, &'a O), String> {
789 let segments: Vec<String> = unit_ref
790 .split('.')
791 .map(|segment| crate::parsing::ast::ascii_lowercase_logical_name(segment.to_string()))
792 .filter(|segment| !segment.is_empty())
793 .collect();
794 if segments.is_empty() {
795 return Err("Unit path is empty".to_string());
796 }
797 let bare = segments
798 .last()
799 .expect("BUG: non-empty segments must have last")
800 .clone();
801 let prefix = &segments[..segments.len() - 1];
802 if prefix.is_empty() {
803 if matches!(bare.as_str(), "percent" | "permille") {
805 if let Some(primitive) = owners
806 .iter()
807 .find(|owner| Arc::ptr_eq(&lemma_type(owner), primitive_ratio_arc()))
808 {
809 return Ok((bare, primitive));
810 }
811 }
812 return match owners {
813 [] => Err(format!(
814 "Unknown unit '{bare}'. Declare it on a measure or ratio type, or import it with uses."
815 )),
816 [only] => Ok((bare, only)),
817 many => {
818 let refs: Vec<&O> = many.iter().collect();
819 if let Ok(only) = try_collapse_equivalent_owners(&refs, &bare, &lemma_type) {
820 Ok((bare, only))
821 } else {
822 Err(format!(
823 "Unit '{bare}' matches more than one type. Write one of: {}",
824 format_qualifier_list(many, &bare, type_name, import_path)
825 ))
826 }
827 }
828 };
829 }
830
831 let mut matches = owners_matching_prefix(owners, prefix, type_name, import_path);
832 dedupe_owner_matches(&mut matches, type_name, import_path);
833 match matches.as_slice() {
834 [only] => Ok((bare, *only)),
835 [] => Err(format!(
836 "Unknown unit '{unit_ref}'. Declare it on a measure or ratio type, or import it with uses."
837 )),
838 many => {
839 if let Ok(only) = try_collapse_equivalent_owners(many, &bare, &lemma_type) {
840 Ok((bare, only))
841 } else {
842 Err(format!(
843 "Unit '{unit_ref}' matches more than one type. Write one of: {}",
844 format_qualifier_list_from_refs(many, &bare, type_name, import_path)
845 ))
846 }
847 }
848 }
849}
850
851fn owners_matching_prefix<'a, O>(
852 owners: &'a [O],
853 prefix: &[String],
854 type_name: impl Fn(&O) -> &str + Copy,
855 import_path: impl Fn(&O) -> &[String] + Copy,
856) -> Vec<&'a O> {
857 if prefix.len() == 1 {
858 let want_type = &prefix[0];
859 let local: Vec<&O> = owners
860 .iter()
861 .filter(|owner| import_path(owner).is_empty() && type_name(owner) == want_type)
862 .collect();
863 if !local.is_empty() {
864 return local;
865 }
866 return owners
867 .iter()
868 .filter(|owner| {
869 let path = import_path(owner);
870 !path.is_empty() && path[0] == *want_type
871 })
872 .collect();
873 }
874
875 if prefix.len() >= 2 {
876 let type_segment = &prefix[prefix.len() - 1];
877 let path_prefix = &prefix[..prefix.len() - 1];
878 let exact: Vec<&O> = owners
879 .iter()
880 .filter(|owner| import_path(owner) == path_prefix && type_name(owner) == type_segment)
881 .collect();
882 if !exact.is_empty() {
883 return exact;
884 }
885 }
886
887 owners
888 .iter()
889 .filter(|owner| {
890 let path = import_path(owner);
891 path.len() >= prefix.len() && path[..prefix.len()] == prefix[..]
892 })
893 .collect()
894}
895
896fn dedupe_owner_matches<O>(
897 matches: &mut Vec<&O>,
898 type_name: impl Fn(&O) -> &str,
899 import_path: impl Fn(&O) -> &[String],
900) {
901 let mut seen = BTreeSet::new();
902 matches
903 .retain(|owner| seen.insert((import_path(owner).to_vec(), type_name(owner).to_string())));
904}
905
906fn format_qualifier_list_from_refs<O>(
907 owners: &[&O],
908 bare: &str,
909 type_name: impl Fn(&O) -> &str,
910 import_path: impl Fn(&O) -> &[String],
911) -> String {
912 let mut paths = BTreeSet::new();
913 for owner in owners {
914 let path = import_path(owner);
915 let type_label = type_name(owner);
916 if path.is_empty() {
917 paths.insert(format!("{type_label}.{bare}"));
918 } else {
919 let full = path
920 .iter()
921 .chain(std::iter::once(&type_label.to_string()))
922 .cloned()
923 .collect::<Vec<_>>()
924 .join(".");
925 paths.insert(format!("{full}.{bare}"));
926 let path_only_unique = owners
927 .iter()
928 .filter(|other| import_path(other) == path)
929 .count()
930 == 1;
931 if path_only_unique {
932 paths.insert(format!("{}.{bare}", path.join(".")));
933 }
934 }
935 }
936 paths.into_iter().collect::<Vec<_>>().join(", ")
937}
938
939fn format_qualifier_list<O>(
940 owners: &[O],
941 bare: &str,
942 type_name: impl Fn(&O) -> &str,
943 import_path: impl Fn(&O) -> &[String],
944) -> String {
945 let mut paths = BTreeSet::new();
946 for owner in owners {
947 let path = import_path(owner);
948 let type_label = type_name(owner);
949 if path.is_empty() {
950 paths.insert(format!("{type_label}.{bare}"));
951 } else {
952 let full = path
953 .iter()
954 .chain(std::iter::once(&type_label.to_string()))
955 .cloned()
956 .collect::<Vec<_>>()
957 .join(".");
958 paths.insert(format!("{full}.{bare}"));
959 let path_only_unique = owners
960 .iter()
961 .filter(|other| import_path(other) == path)
962 .count()
963 == 1;
964 if path_only_unique {
965 paths.insert(format!("{}.{bare}", path.join(".")));
966 }
967 }
968 }
969 paths.into_iter().collect::<Vec<_>>().join(", ")
970}
971
972#[cfg(test)]
973mod tests {
974 use super::*;
975 use crate::computation::rational::rational_one;
976 use crate::literals::{MeasureUnit, MeasureUnits, UnitScale};
977 use crate::parsing::ast::EffectiveDate;
978 use crate::planning::semantics::{
979 LemmaType, SpecVersionKey, TypeExtends, TypeFamily, TypeSpecification,
980 };
981 use indexmap::IndexMap;
982 use std::collections::HashMap;
983 use std::str::FromStr;
984 use std::sync::Arc;
985
986 fn test_spec() -> SpecVersionKey {
987 SpecVersionKey {
988 repository: None,
989 spec: "test".into(),
990 effective: EffectiveDate::Origin,
991 }
992 }
993
994 fn locals(pairs: &[(&str, &Arc<LemmaType>)]) -> HashMap<String, Arc<LemmaType>> {
995 pairs
996 .iter()
997 .map(|(name, lemma_type)| ((*name).to_string(), Arc::clone(lemma_type)))
998 .collect()
999 }
1000
1001 fn bind_locals(planning: PlanningUnitIndex, pairs: &[(&str, &Arc<LemmaType>)]) -> UnitIndex {
1002 let mut resolved = IndexMap::new();
1003 for (name, lemma_type) in pairs {
1004 resolved.insert((*name).to_string(), Arc::clone(lemma_type));
1005 }
1006 planning.bind(&resolved, &test_spec())
1007 }
1008
1009 fn measure_type(name: &str, unit: &str) -> Arc<LemmaType> {
1010 let mut units = MeasureUnits::new();
1011 units.push(MeasureUnit {
1012 name: unit.to_string(),
1013 scale: crate::literals::UnitScale::Literal(rational_one()),
1014 derived_measure_factors: Vec::new(),
1015 decomposition: Default::default(),
1016 minimum: None,
1017 maximum: None,
1018 suggestion_magnitude: None,
1019 });
1020 Arc::new(LemmaType::new(
1021 name.to_string(),
1022 TypeSpecification::Measure {
1023 units,
1024 traits: vec![],
1025 decomposition: None,
1026 minimum: None,
1027 maximum: None,
1028 unit: None,
1029 help: String::new(),
1030 },
1031 TypeExtends::Primitive,
1032 ))
1033 }
1034
1035 fn family_measure_type(family: TypeFamily, units: &[&str]) -> Arc<LemmaType> {
1036 let mut measure_units = MeasureUnits::new();
1037 for unit in units {
1038 measure_units.push(MeasureUnit {
1039 name: (*unit).to_string(),
1040 scale: crate::literals::UnitScale::Literal(rational_one()),
1041 derived_measure_factors: Vec::new(),
1042 decomposition: Default::default(),
1043 minimum: None,
1044 maximum: None,
1045 suggestion_magnitude: None,
1046 });
1047 }
1048 Arc::new(LemmaType::new(
1049 family.name.clone(),
1050 TypeSpecification::Measure {
1051 units: measure_units,
1052 traits: vec![],
1053 decomposition: None,
1054 minimum: None,
1055 maximum: None,
1056 unit: None,
1057 help: String::new(),
1058 },
1059 TypeExtends::custom_local("measure".to_string(), family),
1060 ))
1061 }
1062
1063 #[test]
1064 fn values_dedupes_multiple_bare_units_of_one_owner() {
1065 let money = family_measure_type(
1066 crate::planning::semantics::tests::test_type_family("money"),
1067 &["eur", "cent"],
1068 );
1069 let mut index = UnitIndex::new();
1070 index.insert_owner(
1071 "eur".into(),
1072 UnitOwner::Named {
1073 import_path: Vec::new(),
1074 type_name: "money".into(),
1075 lemma_type: Arc::clone(&money),
1076 },
1077 );
1078 index.insert_owner(
1079 "cent".into(),
1080 UnitOwner::Named {
1081 import_path: Vec::new(),
1082 type_name: "money".into(),
1083 lemma_type: Arc::clone(&money),
1084 },
1085 );
1086 let values: Vec<_> = index.values().collect();
1087 assert_eq!(values.len(), 1);
1088 assert!(Arc::ptr_eq(values[0], &money));
1089 }
1090
1091 #[test]
1092 fn insert_owner_same_slot_replace_updates_distinct_arc() {
1093 let family = crate::planning::semantics::tests::test_type_family("money");
1094 let first = family_measure_type(family.clone(), &["eur"]);
1095 let second = family_measure_type(family.clone(), &["eur"]);
1096 assert!(!Arc::ptr_eq(&first, &second));
1097 let mut index = UnitIndex::new();
1098 index.insert_owner(
1099 "eur".into(),
1100 UnitOwner::Named {
1101 import_path: Vec::new(),
1102 type_name: "money".into(),
1103 lemma_type: Arc::clone(&first),
1104 },
1105 );
1106 index.insert_owner(
1107 "eur".into(),
1108 UnitOwner::Named {
1109 import_path: Vec::new(),
1110 type_name: "money".into(),
1111 lemma_type: Arc::clone(&second),
1112 },
1113 );
1114 let values: Vec<_> = index.values().collect();
1115 assert_eq!(values.len(), 1);
1116 assert!(Arc::ptr_eq(values[0], &second));
1117 assert!(Arc::ptr_eq(
1118 index.family_root(&family).expect("family root"),
1119 &second
1120 ));
1121 }
1122
1123 #[test]
1124 fn family_root_collapses_same_typefamily_across_import_paths() {
1125 let family = crate::planning::semantics::tests::test_type_family("money");
1126 let money = family_measure_type(family.clone(), &["eur"]);
1127 let mut index = UnitIndex::new();
1128 index.insert_owner(
1129 "eur".into(),
1130 UnitOwner::Named {
1131 import_path: vec!["pricing".into()],
1132 type_name: "money".into(),
1133 lemma_type: Arc::clone(&money),
1134 },
1135 );
1136 index.insert_owner(
1137 "eur".into(),
1138 UnitOwner::Named {
1139 import_path: vec!["billing".into()],
1140 type_name: "money".into(),
1141 lemma_type: Arc::clone(&money),
1142 },
1143 );
1144 assert_eq!(index.values().count(), 2);
1145 assert!(Arc::ptr_eq(
1146 index.family_root(&family).expect("one family root"),
1147 &money
1148 ));
1149 }
1150
1151 #[test]
1152 fn family_roots_keep_same_name_families_with_different_identity_distinct() {
1153 let local_family = TypeFamily {
1154 repository: None,
1155 spec: "shop".into(),
1156 effective: EffectiveDate::Origin,
1157 name: "money".into(),
1158 };
1159 let imported_family = TypeFamily {
1160 repository: Some("catalog".into()),
1161 spec: "pricing".into(),
1162 effective: EffectiveDate::DateTimeValue(
1163 crate::DateTimeValue::from_str("2024-01-01T00:00:00Z").expect("date"),
1164 ),
1165 name: "money".into(),
1166 };
1167 let local_money = family_measure_type(local_family.clone(), &["eur"]);
1168 let imported_money = family_measure_type(imported_family.clone(), &["eur"]);
1169 let mut index = UnitIndex::new();
1170 index.insert_owner(
1171 "eur".into(),
1172 UnitOwner::Named {
1173 import_path: Vec::new(),
1174 type_name: "money".into(),
1175 lemma_type: Arc::clone(&local_money),
1176 },
1177 );
1178 index.insert_owner(
1179 "eur".into(),
1180 UnitOwner::Named {
1181 import_path: vec!["pricing".into()],
1182 type_name: "money".into(),
1183 lemma_type: Arc::clone(&imported_money),
1184 },
1185 );
1186 assert!(Arc::ptr_eq(
1187 index.family_root(&local_family).expect("local root"),
1188 &local_money
1189 ));
1190 assert!(Arc::ptr_eq(
1191 index.family_root(&imported_family).expect("imported root"),
1192 &imported_money
1193 ));
1194 assert!(!Arc::ptr_eq(
1195 index.family_root(&local_family).expect("local root"),
1196 index.family_root(&imported_family).expect("imported root")
1197 ));
1198 }
1199
1200 #[test]
1201 fn merge_parent_reclaims_inherited_unit_from_child() {
1202 let mut index = PlanningUnitIndex::new();
1203 let money = measure_type("money", "eur");
1204 let money2 = Arc::new(LemmaType::new(
1205 "money2".to_string(),
1206 money.specifications.clone(),
1207 TypeExtends::custom_local(
1208 "money".to_string(),
1209 crate::planning::semantics::tests::test_type_family("money"),
1210 ),
1211 ));
1212 index
1213 .merge_measure_unit(
1214 "eur".into(),
1215 &money2,
1216 "money2",
1217 &[],
1218 "money",
1219 &locals(&[("money2", &money2)]),
1220 )
1221 .expect("child first");
1222 let bound = bind_locals(index.clone(), &[("money2", &money2)]);
1223 assert_eq!(
1224 bound.unique_owner("eur").map(|t| t.name()),
1225 Some("money2".into())
1226 );
1227 index
1228 .merge_measure_unit(
1229 "eur".into(),
1230 &money,
1231 "money",
1232 &[],
1233 "money",
1234 &locals(&[("money2", &money2), ("money", &money)]),
1235 )
1236 .expect("parent reclaim");
1237 let bound = bind_locals(index, &[("money2", &money2), ("money", &money)]);
1238 assert_eq!(
1239 bound.unique_owner("eur").map(|t| t.name()),
1240 Some("money".into()),
1241 "declarer must reclaim bare ownership"
1242 );
1243 }
1244
1245 #[test]
1246 fn bare_unique_resolves() {
1247 let mut index = UnitIndex::new();
1248 let mass = measure_type("mass", "kilogram");
1249 index.insert_owner(
1250 "kilogram".into(),
1251 UnitOwner::Named {
1252 lemma_type: Arc::clone(&mass),
1253 import_path: vec!["units".into()],
1254 type_name: "mass".into(),
1255 },
1256 );
1257 let (bare, owner) = index.resolve("kilogram").expect("unique");
1258 assert_eq!(bare, "kilogram");
1259 assert_eq!(owner.name(), "mass");
1260 let (bare, owner) = index.resolve("units.kilogram").expect("alias.unit sugar");
1261 assert_eq!(bare, "kilogram");
1262 assert_eq!(owner.name(), "mass");
1263 let (bare, owner) = index.resolve("units.mass.kilogram").expect("full");
1264 assert_eq!(bare, "kilogram");
1265 assert_eq!(owner.name(), "mass");
1266 index
1267 .resolve("mass.kilogram")
1268 .expect_err("Type.unit is local-only for imported owner");
1269 }
1270
1271 #[test]
1272 fn two_segment_import_alias_unit_resolves() {
1273 let mut index = UnitIndex::new();
1274 let mass = measure_type("mass", "kilogram");
1275 index.insert_owner(
1276 "kilogram".into(),
1277 UnitOwner::Named {
1278 lemma_type: Arc::clone(&mass),
1279 import_path: vec!["units".into()],
1280 type_name: "mass".into(),
1281 },
1282 );
1283 index
1284 .resolve("units.kilogram")
1285 .expect("alias.unit resolves when unique under alias");
1286 index
1287 .resolve("units.mass.kilogram")
1288 .expect("alias.Type.unit must resolve");
1289 }
1290
1291 #[test]
1292 fn nested_import_path_type_unit_resolves() {
1293 let mut index = UnitIndex::new();
1294 let mass = measure_type("mass", "kilogram");
1295 index.insert_owner(
1296 "kilogram".into(),
1297 UnitOwner::Named {
1298 lemma_type: Arc::clone(&mass),
1299 import_path: vec!["a".into(), "b".into()],
1300 type_name: "mass".into(),
1301 },
1302 );
1303 index
1304 .resolve("a.b.mass.kilogram")
1305 .expect("nested alias.Type.unit");
1306 }
1307
1308 #[test]
1309 fn merge_second_measure_declarer_keeps_both_and_bare_use_must_qualify() {
1310 let mut index = PlanningUnitIndex::new();
1311 let a = measure_type("money_a", "eur");
1312 let b = measure_type("money_b", "eur");
1313 index
1314 .merge_measure_unit(
1315 "eur".into(),
1316 &a,
1317 "money_a",
1318 &[],
1319 "money_a",
1320 &locals(&[("money_a", &a)]),
1321 )
1322 .expect("first declarer");
1323 index
1324 .merge_measure_unit(
1325 "eur".into(),
1326 &b,
1327 "money_b",
1328 &[],
1329 "money_b",
1330 &locals(&[("money_a", &a), ("money_b", &b)]),
1331 )
1332 .expect("second independent eur declarer is kept");
1333 assert_eq!(index.owners_for("eur").len(), 2);
1334
1335 let bound = bind_locals(index, &[("money_a", &a), ("money_b", &b)]);
1336 let err = bound
1337 .resolve("eur")
1338 .expect_err("bare eur with two declarers must be qualified");
1339 assert!(
1340 err.contains("money_a.eur") && err.contains("money_b.eur"),
1341 "got: {err}"
1342 );
1343 let (bare, owner) = bound.resolve("money_b.eur").expect("qualified eur");
1344 assert_eq!(bare, "eur");
1345 assert_eq!(owner.name(), "money_b");
1346 }
1347
1348 #[test]
1349 fn ratio_merge_inserts_second_alias_for_same_type() {
1350 let mut index = PlanningUnitIndex::new();
1351 let ratio = {
1352 let mut units = crate::literals::RatioUnits::new();
1353 units.push(crate::literals::RatioUnit {
1354 name: "percent".into(),
1355 value: rational_one(),
1356 minimum: None,
1357 maximum: None,
1358 suggestion_magnitude: None,
1359 });
1360 Arc::new(LemmaType::new(
1361 "rate".into(),
1362 TypeSpecification::Ratio {
1363 minimum: None,
1364 maximum: None,
1365 units,
1366 unit: None,
1367 help: String::new(),
1368 },
1369 TypeExtends::Primitive,
1370 ))
1371 };
1372 index
1373 .merge_ratio_unit(
1374 "percent".into(),
1375 &ratio,
1376 "rate",
1377 &["alpha".into()],
1378 &HashMap::new(),
1379 )
1380 .expect("first alias");
1381 index
1382 .merge_ratio_unit(
1383 "percent".into(),
1384 &ratio,
1385 "rate",
1386 &["beta".into()],
1387 &HashMap::new(),
1388 )
1389 .expect("second alias");
1390 let bound = bind_locals(index, &[]);
1391 assert_eq!(bound.owners_for("percent").len(), 2);
1392 bound
1393 .resolve("alpha.percent")
1394 .expect("alias.unit sugar for ratio");
1395 bound.resolve("beta.percent").expect("beta alias.unit");
1396 bound
1397 .resolve("alpha.rate.percent")
1398 .expect("alpha.Type.unit");
1399 bound.resolve("beta.rate.percent").expect("beta.Type.unit");
1400 let type_unit_err = bound
1401 .resolve("rate.percent")
1402 .expect_err("Type.unit ambiguous when same type_name appears under two aliases");
1403 assert!(
1404 type_unit_err.contains("more than one type")
1405 || type_unit_err.contains("Write one of")
1406 || type_unit_err.contains("ambiguous")
1407 || type_unit_err.contains("Qualify")
1408 || type_unit_err.contains("Unknown"),
1409 "got: {type_unit_err}"
1410 );
1411 }
1412
1413 #[test]
1414 fn named_ratio_keeps_primitive_percent_and_bare_picks_it() {
1415 let mut index = PlanningUnitIndex::new();
1416 index.insert_owner("percent".into(), PlanningOwner::PrimitiveRatio);
1417 let ratio = {
1418 let mut units = crate::literals::RatioUnits::new();
1419 units.push(crate::literals::RatioUnit {
1420 name: "percent".into(),
1421 value: rational_one(),
1422 minimum: None,
1423 maximum: None,
1424 suggestion_magnitude: None,
1425 });
1426 Arc::new(LemmaType::new(
1427 "margin".into(),
1428 TypeSpecification::Ratio {
1429 minimum: None,
1430 maximum: None,
1431 units,
1432 unit: None,
1433 help: String::new(),
1434 },
1435 TypeExtends::Primitive,
1436 ))
1437 };
1438 index
1439 .merge_ratio_unit(
1440 "percent".into(),
1441 &ratio,
1442 "margin",
1443 &[],
1444 &locals(&[("margin", &ratio)]),
1445 )
1446 .expect("named ratio merges beside builtin");
1447 let bound = bind_locals(index, &[("margin", &ratio)]);
1448 assert_eq!(bound.owners_for("percent").len(), 2);
1449 let (bare, owner) = bound.resolve("percent").expect("bare percent");
1450 assert_eq!(bare, "percent");
1451 assert!(Arc::ptr_eq(&owner, primitive_ratio_arc()));
1452 let (_, qualified) = bound.resolve("margin.percent").expect("Type.unit");
1453 assert!(Arc::ptr_eq(&qualified, &ratio));
1454 }
1455
1456 fn measure_type_scale(name: &str, unit: &str, scale: UnitScale) -> Arc<LemmaType> {
1457 let mut units = MeasureUnits::new();
1458 units.push(MeasureUnit {
1459 name: unit.to_string(),
1460 scale,
1461 derived_measure_factors: Vec::new(),
1462 decomposition: Default::default(),
1463 minimum: None,
1464 maximum: None,
1465 suggestion_magnitude: None,
1466 });
1467 Arc::new(LemmaType::new(
1468 name.to_string(),
1469 TypeSpecification::Measure {
1470 units,
1471 traits: vec![],
1472 decomposition: None,
1473 minimum: None,
1474 maximum: None,
1475 unit: None,
1476 help: String::new(),
1477 },
1478 TypeExtends::custom_local(
1479 "money".to_string(),
1480 crate::planning::semantics::tests::test_type_family("money"),
1481 ),
1482 ))
1483 }
1484
1485 fn reference_scale(name: &str) -> UnitScale {
1486 UnitScale::Unresolved {
1487 written: name.to_string(),
1488 segments: Vec::new(),
1489 name: name.to_string(),
1490 }
1491 }
1492
1493 #[test]
1494 fn merge_same_literal_scale_agrees() {
1495 let mut index = PlanningUnitIndex::new();
1496 let money = measure_type("money", "eur");
1497 index
1498 .merge_measure_unit(
1499 "eur".into(),
1500 &money,
1501 "money",
1502 &[],
1503 "money",
1504 &locals(&[("money", &money)]),
1505 )
1506 .expect("first");
1507 index
1508 .merge_measure_unit(
1509 "eur".into(),
1510 &money,
1511 "money",
1512 &[],
1513 "money",
1514 &locals(&[("money", &money)]),
1515 )
1516 .expect("same literal scale");
1517 }
1518
1519 #[test]
1520 fn merge_same_consumer_reference_scale_agrees() {
1521 let mut index = PlanningUnitIndex::new();
1522 let left = measure_type_scale("left", "gbp", reference_scale("rate"));
1523 let right = measure_type_scale("right", "gbp", reference_scale("rate"));
1524 index
1525 .merge_measure_unit(
1526 "gbp".into(),
1527 &left,
1528 "left",
1529 &[],
1530 "money",
1531 &locals(&[("left", &left)]),
1532 )
1533 .expect("first");
1534 index
1535 .merge_measure_unit(
1536 "gbp".into(),
1537 &right,
1538 "right",
1539 &[],
1540 "money",
1541 &locals(&[("left", &left), ("right", &right)]),
1542 )
1543 .expect("same consumer-coordinate reference");
1544 }
1545
1546 #[test]
1547 fn merge_literal_and_reference_conflict() {
1548 let mut index = PlanningUnitIndex::new();
1549 let literal = measure_type_scale("money", "gbp", UnitScale::Literal(rational_one()));
1550 let reference = measure_type_scale("money", "gbp", reference_scale("rate"));
1551 index
1552 .merge_measure_unit(
1553 "gbp".into(),
1554 &literal,
1555 "money",
1556 &["a".into()],
1557 "money",
1558 &locals(&[("money", &literal)]),
1559 )
1560 .expect("literal");
1561 let err = index
1562 .merge_measure_unit(
1563 "gbp".into(),
1564 &reference,
1565 "money",
1566 &["b".into()],
1567 "money",
1568 &locals(&[("money", &reference)]),
1569 )
1570 .expect_err("literal vs reference");
1571 assert_eq!(
1572 err,
1573 UnitMergeConflict::ConflictingFactors {
1574 unit: "gbp".into(),
1575 family: "money".into(),
1576 }
1577 );
1578 }
1579
1580 #[test]
1581 fn merge_different_reference_paths_conflict() {
1582 let mut index = PlanningUnitIndex::new();
1583 let money = measure_type_scale("money", "gbp", reference_scale("rate"));
1584 index
1585 .merge_measure_unit(
1586 "gbp".into(),
1587 &money,
1588 "money",
1589 &["a".into()],
1590 "money",
1591 &locals(&[("money", &money)]),
1592 )
1593 .expect("alias a");
1594 let err = index
1595 .merge_measure_unit(
1596 "gbp".into(),
1597 &money,
1598 "money",
1599 &["b".into()],
1600 "money",
1601 &locals(&[("money", &money)]),
1602 )
1603 .expect_err("alias b different consumer path");
1604 assert_eq!(
1605 err,
1606 UnitMergeConflict::ConflictingFactors {
1607 unit: "gbp".into(),
1608 family: "money".into(),
1609 }
1610 );
1611 }
1612}