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