Skip to main content

lemma/planning/
types.rs

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