Skip to main content

lemma/planning/
unit_index.rs

1//! Expression-scope unit index: every declarer of each bare unit name in scope.
2//! Qualify with `Type.unit`, `alias.Type.unit`, nested import paths, or `alias.unit`
3//! when that import scope identifies the owner.
4
5use 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/// Owner recorded while types are still being resolved. Local types are names only;
13/// imported types keep the Arc the other spec already finalized.
14#[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    /// Local bodies come from `resolved`. Imported bodies are the other spec's final Arc.
57    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/// Bound owner after magnitudes: eval and Show read Arcs from here.
73#[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/// Conflict while merging a unit owner into the index during type resolution.
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum UnitMergeConflict {
112    ConflictingFactors { unit: String, family: String },
113}
114
115/// Names of unit owners while `resolved` is still being mutated.
116#[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_scale(new_unit)
214                        });
215                    if same_factor {
216                        // Same family and same scale: keep one local declarer
217                        // (extension chain). Distinct scales are a conflict below.
218                        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        // Keep PrimitiveRatio for bare percent/permille; named owners stay for Type.unit.
266        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    /// Attach local Arcs from final `resolved` and add missing family-root units.
351    #[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/// Expression-scope units after bind: every declarer per bare unit name.
436#[derive(Debug, Clone, Default)]
437pub struct UnitIndex {
438    by_bare: IndexMap<String, Vec<UnitOwner>>,
439    /// First-seen owning type per `(import_path, type_name)` slot.
440    distinct_types: IndexMap<(Vec<String>, String), Arc<LemmaType>>,
441    /// Family root Arc: type whose `.name == family.name`, keyed by full [`TypeFamily`].
442    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    /// Family-root type for `family`, keyed by full [`TypeFamily`] identity.
484    #[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_scale_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            .map(|unit| UnitScalar::Measure(unit.scale.clone())),
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::literals::UnitScale),
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_scale_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_scale_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        // Builtin percent/permille always bind PrimitiveRatio; named owners stay for Type.unit.
804        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, PathSegment, 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::Reference {
1487            factors: vec![(
1488                crate::planning::semantics::ReferenceTarget::Data(
1489                    crate::planning::semantics::DataPath::local(name.to_string()),
1490                ),
1491                1,
1492            )],
1493            coefficient: crate::computation::rational::rational_one(),
1494        }
1495    }
1496
1497    #[test]
1498    fn merge_same_literal_scale_agrees() {
1499        let mut index = PlanningUnitIndex::new();
1500        let money = measure_type("money", "eur");
1501        index
1502            .merge_measure_unit(
1503                "eur".into(),
1504                &money,
1505                "money",
1506                &[],
1507                "money",
1508                &locals(&[("money", &money)]),
1509            )
1510            .expect("first");
1511        index
1512            .merge_measure_unit(
1513                "eur".into(),
1514                &money,
1515                "money",
1516                &[],
1517                "money",
1518                &locals(&[("money", &money)]),
1519            )
1520            .expect("same literal scale");
1521    }
1522
1523    #[test]
1524    fn merge_same_reference_scale_agrees() {
1525        let mut index = PlanningUnitIndex::new();
1526        let left = measure_type_scale("left", "gbp", reference_scale("rate"));
1527        let right = measure_type_scale("right", "gbp", reference_scale("rate"));
1528        index
1529            .merge_measure_unit(
1530                "gbp".into(),
1531                &left,
1532                "left",
1533                &[],
1534                "money",
1535                &locals(&[("left", &left)]),
1536            )
1537            .expect("first");
1538        index
1539            .merge_measure_unit(
1540                "gbp".into(),
1541                &right,
1542                "right",
1543                &[],
1544                "money",
1545                &locals(&[("left", &left), ("right", &right)]),
1546            )
1547            .expect("same reference scale");
1548    }
1549
1550    #[test]
1551    fn merge_literal_and_reference_conflict() {
1552        let mut index = PlanningUnitIndex::new();
1553        let literal = measure_type_scale("left", "gbp", UnitScale::Literal(rational_one()));
1554        let reference = measure_type_scale("right", "gbp", reference_scale("rate"));
1555        index
1556            .merge_measure_unit(
1557                "gbp".into(),
1558                &literal,
1559                "left",
1560                &[],
1561                "money",
1562                &locals(&[("left", &literal)]),
1563            )
1564            .expect("literal");
1565        let err = index
1566            .merge_measure_unit(
1567                "gbp".into(),
1568                &reference,
1569                "right",
1570                &[],
1571                "money",
1572                &locals(&[("left", &literal), ("right", &reference)]),
1573            )
1574            .expect_err("literal vs reference");
1575        assert_eq!(
1576            err,
1577            UnitMergeConflict::ConflictingFactors {
1578                unit: "gbp".into(),
1579                family: "money".into(),
1580            }
1581        );
1582    }
1583
1584    #[test]
1585    fn merge_different_reference_scales_conflict() {
1586        let mut index = PlanningUnitIndex::new();
1587        let left = measure_type_scale("left", "gbp", reference_scale("rate_left"));
1588        let right = measure_type_scale("right", "gbp", reference_scale("rate_right"));
1589        index
1590            .merge_measure_unit(
1591                "gbp".into(),
1592                &left,
1593                "left",
1594                &[],
1595                "money",
1596                &locals(&[("left", &left)]),
1597            )
1598            .expect("rate_left");
1599        let err = index
1600            .merge_measure_unit(
1601                "gbp".into(),
1602                &right,
1603                "right",
1604                &[],
1605                "money",
1606                &locals(&[("left", &left), ("right", &right)]),
1607            )
1608            .expect_err("different reference scales in one family conflict");
1609        assert_eq!(
1610            err,
1611            UnitMergeConflict::ConflictingFactors {
1612                unit: "gbp".into(),
1613                family: "money".into(),
1614            }
1615        );
1616    }
1617
1618    #[test]
1619    fn two_imported_owners_of_one_dynamic_unit_stay_distinct() {
1620        let declared = measure_type_scale("money", "gbp", reference_scale("rate"));
1621        let money_a = Arc::new(declared.with_scale_import_hop(&PathSegment {
1622            uses: Arc::from("a"),
1623            row: None,
1624            repository: None,
1625            spec: Arc::from("priced"),
1626        }));
1627        let money_b = Arc::new(declared.with_scale_import_hop(&PathSegment {
1628            uses: Arc::from("b"),
1629            row: None,
1630            repository: None,
1631            spec: Arc::from("priced"),
1632        }));
1633        let mut index = PlanningUnitIndex::new();
1634        index.insert_owner(
1635            "gbp".to_string(),
1636            PlanningOwner::Imported {
1637                import_path: vec!["a".to_string()],
1638                type_name: "money".to_string(),
1639                lemma_type: Arc::clone(&money_a),
1640            },
1641        );
1642        index.insert_owner(
1643            "gbp".to_string(),
1644            PlanningOwner::Imported {
1645                import_path: vec!["b".to_string()],
1646                type_name: "money".to_string(),
1647                lemma_type: Arc::clone(&money_b),
1648            },
1649        );
1650        let bound = bind_locals(index, &[]);
1651        let error = bound.resolve("gbp").expect_err("bare gbp is ambiguous");
1652        assert!(
1653            error.contains("Unit 'gbp' matches more than one type")
1654                && error.contains("a.money.gbp")
1655                && error.contains("b.money.gbp"),
1656            "two instances of one spec demand qualification, got: {error}"
1657        );
1658        let (_, owner) = bound
1659            .resolve_owner("a.money.gbp")
1660            .expect("qualification by alias a");
1661        assert_eq!(owner.import_path(), &["a".to_string()][..]);
1662        let (_, owner) = bound
1663            .resolve_owner("b.money.gbp")
1664            .expect("qualification by alias b");
1665        assert_eq!(owner.import_path(), &["b".to_string()][..]);
1666    }
1667}