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