Skip to main content

lisette_semantics/
store.rs

1use std::path::PathBuf;
2use std::sync::atomic::{AtomicU32, Ordering};
3
4use ecow::EcoString;
5use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
6
7use syntax::ast::{EnumVariant, Expression, Literal, StructFieldDefinition};
8use syntax::program::{
9    Definition, DefinitionBody, File, Interface, MethodSignatures, Module, ModuleId,
10};
11use syntax::types::{SimpleKind, SubstitutionMap, Symbol, Type, substitute};
12
13pub const ENTRY_MODULE_ID: &str = "_entry_";
14pub const ENTRY_FILE_ID: u32 = 0;
15
16#[derive(Debug, Clone)]
17pub struct ClosedMember {
18    /// Qualified the way the user writes it (e.g. `time.Sunday`), for the diagnostic.
19    pub display_name: EcoString,
20    /// The member's source literal, for rendering the valid-set hint.
21    pub literal: Literal,
22    /// The comparable form, derived once so membership and sort never disagree.
23    pub value: DomainValue,
24}
25
26/// The curated valid-value set of a `#[go(closed_domain)]` named primitive.
27#[derive(Debug, Clone)]
28pub struct ClosedDomain {
29    pub base: SimpleKind,
30    pub type_display: EcoString,
31    pub members: Vec<ClosedMember>,
32}
33
34/// A literal reduced to its comparable form for a closed domain's base kind.
35/// Float bases are not indexed, so only integers (signed `i128`) and strings occur.
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
37pub enum DomainValue {
38    Int(i128),
39    Str(String),
40}
41
42impl DomainValue {
43    pub fn from_literal(literal: &Literal, base: SimpleKind) -> Option<DomainValue> {
44        // `rune` is a signed integer kind, so handle it before the integer arm
45        // to accept char literals as codepoints. A negative const is stored as
46        // its two's-complement `u64`, so signed bases reinterpret it as `i64`.
47        match base {
48            SimpleKind::Rune => match literal {
49                Literal::Char(text) => char_codepoint(text).map(|cp| DomainValue::Int(cp as i128)),
50                Literal::Integer { value, .. } => Some(DomainValue::Int(*value as i64 as i128)),
51                _ => None,
52            },
53            SimpleKind::String => match literal {
54                Literal::String { value, .. } => Some(DomainValue::Str(value.clone())),
55                _ => None,
56            },
57            _ if is_unsigned_base(base) => match literal {
58                Literal::Integer { value, .. } => Some(DomainValue::Int(*value as i128)),
59                _ => None,
60            },
61            _ if base.is_signed_int() => match literal {
62                Literal::Integer { value, .. } => Some(DomainValue::Int(*value as i64 as i128)),
63                _ => None,
64            },
65            _ => None,
66        }
67    }
68}
69
70/// `uintptr` is an unsigned integer for value purposes but is excluded from
71/// `SimpleKind::is_unsigned_int`, so it is folded in here.
72pub fn is_unsigned_base(base: SimpleKind) -> bool {
73    base.is_unsigned_int() || base == SimpleKind::Uintptr
74}
75
76/// Decodes a rune literal's inner text to a codepoint, covering the escapes the
77/// lexer accepts (`\a \b \f \n \r \t \v \\ \'`, `\x` hex, and octal `\NNN`).
78fn char_codepoint(text: &str) -> Option<u64> {
79    let Some(rest) = text.strip_prefix('\\') else {
80        return text.chars().next().map(|c| c as u64);
81    };
82    match rest.as_bytes().first()? {
83        b'a' => Some(7),
84        b'b' => Some(8),
85        b'f' => Some(12),
86        b'n' => Some(10),
87        b'r' => Some(13),
88        b't' => Some(9),
89        b'v' => Some(11),
90        b'\\' => Some(92),
91        b'\'' => Some(39),
92        b'x' => u64::from_str_radix(&rest[1..], 16).ok(),
93        b'0'..=b'7' => u64::from_str_radix(rest, 8).ok(),
94        _ => None,
95    }
96}
97
98pub struct Store {
99    pub modules: HashMap<String, Module>,
100    pub module_ids: Vec<ModuleId>,
101    /// file ID -> module ID
102    pub files: HashMap<u32, String>,
103    /// Go module ID -> Go package name, from the typedef `// Package:` directive.
104    /// Present only when the package name differs from the final path segment.
105    pub go_package_names: HashMap<String, String>,
106    /// File ID -> on-disk path of the `.d.lis` typedef. Lets the LSP map go: typedef
107    /// file IDs to the actual cache path so go-to-definition can navigate there.
108    pub typedef_paths: HashMap<u32, PathBuf>,
109    visited_modules: HashSet<String>,
110    /// File ID counter. Starts at 2 because 0 is reserved for entry, 1 for prelude.
111    next_file_id: AtomicU32,
112    /// Closed-domain index, keyed by the type's qualified name (the `id` in
113    /// `Type::Nominal`). Built once after registration by `build_closed_domains`.
114    pub closed_domains: HashMap<Symbol, ClosedDomain>,
115}
116
117impl Default for Store {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123impl Store {
124    pub fn new() -> Self {
125        let prelude_module = Module::new("prelude");
126        let nominal_module = Module::nominal();
127
128        let modules = vec![
129            (prelude_module.id.clone(), prelude_module),
130            (nominal_module.id.clone(), nominal_module),
131        ]
132        .into_iter()
133        .collect();
134
135        let module_ids = vec!["prelude".to_string()];
136
137        Self {
138            files: Default::default(),
139            modules,
140            module_ids,
141            go_package_names: Default::default(),
142            typedef_paths: Default::default(),
143            visited_modules: Default::default(),
144            next_file_id: AtomicU32::new(2), // 0 = entrypoint, 1 = prelude
145            closed_domains: Default::default(),
146        }
147    }
148
149    pub fn new_file_id(&self) -> u32 {
150        self.next_file_id.fetch_add(1, Ordering::Relaxed)
151    }
152
153    pub fn register_file(&mut self, file_id: u32, module_id: &str) {
154        self.files.insert(file_id, module_id.to_string());
155    }
156
157    pub fn entry_module_id(&self) -> &'static str {
158        ENTRY_MODULE_ID
159    }
160
161    /// Initializes the entry module with reserved file ID 0.
162    pub fn init_entry_module(&mut self) {
163        self.add_module(ENTRY_MODULE_ID);
164        self.register_file(ENTRY_FILE_ID, ENTRY_MODULE_ID);
165    }
166
167    pub fn store_entry_file(
168        &mut self,
169        filename: &str,
170        display_path: &str,
171        source: &str,
172        ast: Vec<Expression>,
173    ) {
174        self.store_file(
175            ENTRY_MODULE_ID,
176            File {
177                id: ENTRY_FILE_ID,
178                module_id: ENTRY_MODULE_ID.to_string(),
179                name: filename.to_string(),
180                display_path: display_path.to_string(),
181                source: source.to_string(),
182                items: ast,
183            },
184        );
185    }
186
187    pub fn store_module(&mut self, module_id: &str, files: Vec<File>) {
188        self.mark_visited(module_id);
189        self.add_module(module_id);
190
191        for file in files {
192            self.store_file(module_id, file);
193        }
194    }
195
196    /// Stores a file in the module and registers the file_id -> module_id mapping.
197    /// .d.lis files go to `typedefs`, .lis files go to `files`.
198    pub fn store_file(&mut self, module_id: &str, file: File) {
199        self.files.insert(file.id, module_id.to_string());
200
201        let module = self
202            .get_module_mut(module_id)
203            .expect("module must exist to store file");
204
205        if file.is_d_lis() {
206            module.typedefs.insert(file.id, file);
207        } else {
208            module.files.insert(file.id, file);
209        }
210    }
211
212    pub fn get_file(&self, file_id: u32) -> Option<&File> {
213        let module_id = self.files.get(&file_id)?;
214        let module = self.get_module(module_id)?;
215        module
216            .get_file(file_id)
217            .or_else(|| module.get_typedef_by_id(file_id))
218    }
219
220    pub fn get_file_mut(&mut self, file_id: u32) -> Option<&mut File> {
221        let module_id = self.files.get(&file_id)?.clone();
222        let module = self.modules.get_mut(&module_id)?;
223        module
224            .files
225            .get_mut(&file_id)
226            .or_else(|| module.typedefs.get_mut(&file_id))
227    }
228
229    pub fn get_module(&self, module_id: &str) -> Option<&Module> {
230        self.modules.get(module_id)
231    }
232
233    pub fn has(&self, module_id: &str) -> bool {
234        self.modules.contains_key(module_id)
235    }
236
237    pub fn add_module(&mut self, module_id: &str) {
238        if self.modules.contains_key(module_id) {
239            return;
240        }
241
242        self.modules
243            .insert(module_id.to_string(), Module::new(module_id));
244        self.module_ids.push(module_id.to_string());
245    }
246
247    pub fn get_module_mut(&mut self, module_id: &str) -> Option<&mut Module> {
248        self.modules.get_mut(module_id)
249    }
250
251    pub fn is_visited(&self, module_id: &str) -> bool {
252        self.visited_modules.contains(module_id)
253    }
254
255    pub fn mark_visited(&mut self, module_id: &str) {
256        self.visited_modules.insert(module_id.to_string());
257    }
258
259    pub fn get_definition(&self, qualified_name: &str) -> Option<&Definition> {
260        let module_name = self.module_for_qualified_name(qualified_name)?;
261
262        self.get_module(module_name)?
263            .definitions
264            .get(qualified_name)
265    }
266
267    pub fn module_for_qualified_name<'a>(&'a self, qualified_name: &'a str) -> Option<&'a str> {
268        syntax::types::module_for_qualified_name(
269            qualified_name,
270            self.modules.keys().map(String::as_str),
271        )
272    }
273
274    pub fn variants_of(&self, qualified_name: &str) -> Option<&[EnumVariant]> {
275        match &self.get_definition(qualified_name)?.body {
276            DefinitionBody::Enum { variants, .. } => Some(variants),
277            _ => None,
278        }
279    }
280
281    pub fn variant_of(&self, enum_qualified: &str, variant_name: &str) -> Option<&EnumVariant> {
282        self.variants_of(enum_qualified)?
283            .iter()
284            .find(|v| v.name == variant_name)
285    }
286
287    pub fn is_nominal_defined_type(&self, qualified_name: &str) -> bool {
288        match self.get_definition(qualified_name) {
289            Some(def) => def.is_newtype(),
290            None => false,
291        }
292    }
293
294    pub fn build_closed_domains(&mut self) {
295        // type id -> (base kind, id of the module that declares the type)
296        let mut bases: HashMap<Symbol, (SimpleKind, String)> = HashMap::default();
297        for module in self.modules.values() {
298            for (qualified_name, definition) in &module.definitions {
299                // Float domains rely on exact-equality over fragile values and do
300                // not occur in the Go stdlib; they are deliberately not indexed.
301                if definition.is_closed_domain()
302                    && let Some(base) = definition.ty().underlying_simple_kind()
303                    && !base.is_float()
304                {
305                    bases.insert(qualified_name.clone(), (base, module.id.clone()));
306                }
307            }
308        }
309
310        if bases.is_empty() {
311            return;
312        }
313
314        let mut members: HashMap<Symbol, Vec<ClosedMember>> = HashMap::default();
315        for module in self.modules.values() {
316            for (qualified_name, definition) in &module.definitions {
317                let Some(const_literal) = definition.const_value() else {
318                    continue;
319                };
320                let Type::Nominal { id, .. } = definition.ty() else {
321                    continue;
322                };
323                let Some((base, declaring_module)) = bases.get(id) else {
324                    continue;
325                };
326                // Only consts declared alongside the type extend its domain; a
327                // const of an imported closed type in user code must not widen it.
328                if module.id != *declaring_module {
329                    continue;
330                }
331                let Some(value) = DomainValue::from_literal(const_literal, *base) else {
332                    continue;
333                };
334                members.entry(id.clone()).or_default().push(ClosedMember {
335                    display_name: domain_display_name(qualified_name.as_str()).into(),
336                    literal: const_literal.clone(),
337                    value,
338                });
339            }
340        }
341
342        let mut domains: HashMap<Symbol, ClosedDomain> = HashMap::default();
343        for (type_id, (base, _)) in bases {
344            let Some(mut domain_members) = members.remove(&type_id) else {
345                continue;
346            };
347            domain_members.sort_by(|a, b| a.value.cmp(&b.value));
348            domains.insert(
349                type_id.clone(),
350                ClosedDomain {
351                    base,
352                    type_display: domain_display_name(type_id.as_str()).into(),
353                    members: domain_members,
354                },
355            );
356        }
357
358        self.closed_domains = domains;
359    }
360
361    pub fn fields_of(&self, qualified_name: &str) -> Option<&[StructFieldDefinition]> {
362        match &self.get_definition(qualified_name)?.body {
363            DefinitionBody::Struct { fields, .. } => Some(fields),
364            _ => None,
365        }
366    }
367
368    pub fn struct_kind(&self, qualified_name: &str) -> Option<syntax::ast::StructKind> {
369        match &self.get_definition(qualified_name)?.body {
370            DefinitionBody::Struct { kind, .. } => Some(*kind),
371            _ => None,
372        }
373    }
374
375    pub fn struct_constructor(&self, qualified_name: &str) -> Option<&Type> {
376        match &self.get_definition(qualified_name)?.body {
377            DefinitionBody::Struct { constructor, .. } => constructor.as_ref(),
378            _ => None,
379        }
380    }
381
382    pub fn parent_interfaces_of(&self, qualified_name: &str) -> Option<&[Type]> {
383        match &self.get_definition(qualified_name)?.body {
384            DefinitionBody::Interface { definition, .. } => Some(&definition.parents),
385            _ => None,
386        }
387    }
388
389    pub fn get_type(&self, qualified_name: &str) -> Option<&Type> {
390        self.get_definition(qualified_name)
391            .map(|definition| definition.ty())
392    }
393
394    pub fn get_interface(&self, qualified_name: &str) -> Option<&Interface> {
395        match &self.get_definition(qualified_name)?.body {
396            DefinitionBody::Interface { definition, .. } => Some(definition),
397            _ => None,
398        }
399    }
400
401    pub fn is_interface(&self, ty: &Type) -> bool {
402        matches!(ty, Type::Nominal { id, .. } if self.get_interface(id.as_str()).is_some())
403    }
404
405    pub fn is_nilable_go_type(&self, ty: &Type) -> bool {
406        if ty.is_ref() || matches!(ty, Type::Function(_)) {
407            return true;
408        }
409        let Type::Nominal { id, .. } = ty else {
410            return false;
411        };
412        if self.get_definition(id.as_str()).is_none() {
413            return false;
414        }
415        if self.get_interface(id.as_str()).is_some() {
416            return true;
417        }
418        match ty.get_underlying() {
419            Some(Type::Function(_)) => true,
420            Some(u) if u.is_ref() => true,
421            _ => false,
422        }
423    }
424
425    pub fn peel_alias(&self, ty: &Type) -> Type {
426        syntax::types::peel_alias(ty, |id| {
427            self.get_definition(id)
428                .is_some_and(Definition::is_type_alias)
429        })
430    }
431
432    pub fn deep_resolve_alias(&self, ty: &Type) -> Type {
433        let mut current = ty.clone();
434        let mut seen: HashSet<Symbol> = HashSet::default();
435        loop {
436            let Type::Nominal { id, params, .. } = &current else {
437                return current;
438            };
439            if !seen.insert(id.clone()) {
440                return current;
441            }
442            let Some(def) = self.get_definition(id.as_str()) else {
443                return current;
444            };
445            if !matches!(def.body, DefinitionBody::TypeAlias { .. }) {
446                return current;
447            }
448            let def_ty = &def.ty;
449            let (vars, body) = match def_ty {
450                Type::Forall { vars, body } => (vars.clone(), body.as_ref().clone()),
451                other => (vec![], other.clone()),
452            };
453            let map: SubstitutionMap = vars.iter().cloned().zip(params.iter().cloned()).collect();
454            current = substitute(&body, &map);
455        }
456    }
457
458    pub fn peel_alias_deep(&self, ty: &Type) -> Type {
459        match self.peel_alias(ty) {
460            Type::Compound { kind, args } => Type::Compound {
461                kind,
462                args: args.iter().map(|a| self.peel_alias_deep(a)).collect(),
463            },
464            Type::Tuple(elements) => {
465                Type::Tuple(elements.iter().map(|e| self.peel_alias_deep(e)).collect())
466            }
467            Type::Nominal {
468                id,
469                params,
470                underlying_ty,
471            } => Type::Nominal {
472                id,
473                params: params.iter().map(|p| self.peel_alias_deep(p)).collect(),
474                underlying_ty,
475            },
476            Type::Function(f) => {
477                let f = std::sync::Arc::try_unwrap(f).unwrap_or_else(|arc| (*arc).clone());
478                Type::function(
479                    f.params.iter().map(|p| self.peel_alias_deep(p)).collect(),
480                    f.param_mutability,
481                    f.bounds,
482                    Box::new(self.peel_alias_deep(&f.return_type)),
483                )
484            }
485            other => other,
486        }
487    }
488
489    pub fn get_own_methods(&self, qualified_name: &str) -> Option<&MethodSignatures> {
490        match &self.get_definition(qualified_name)?.body {
491            DefinitionBody::Struct { methods, .. } => Some(methods),
492            DefinitionBody::TypeAlias { methods, .. } => Some(methods),
493            DefinitionBody::Enum { methods, .. } => Some(methods),
494            _ => None,
495        }
496    }
497
498    pub fn get_all_methods(
499        &self,
500        ty: &Type,
501        trait_bounds: &HashMap<Symbol, Vec<Type>>,
502    ) -> MethodSignatures {
503        let mut visited = HashSet::default();
504        self.get_all_methods_recursive(ty, trait_bounds, &mut visited)
505    }
506
507    fn get_all_methods_recursive(
508        &self,
509        ty: &Type,
510        trait_bounds: &HashMap<Symbol, Vec<Type>>,
511        visited: &mut HashSet<String>,
512    ) -> MethodSignatures {
513        let stripped = ty.strip_refs();
514        let Some(qualified_name) = method_lookup_key(&stripped) else {
515            return MethodSignatures::default();
516        };
517
518        // Cyclic embeddings survive registration as an error with parents intact; guard the walk.
519        if !visited.insert(qualified_name.as_str().to_string()) {
520            return MethodSignatures::default();
521        }
522
523        if let Some(interface) = self.get_interface(&qualified_name) {
524            let mut all_interface_methods = MethodSignatures::default();
525
526            let type_args = ty.get_type_params().unwrap_or_default();
527            let map: SubstitutionMap = interface
528                .generics
529                .iter()
530                .map(|g| g.name.clone())
531                .zip(type_args.iter().cloned())
532                .collect();
533
534            for (name, method_ty) in &interface.methods {
535                let substituted = substitute(method_ty, &map);
536                all_interface_methods.insert(name.clone(), substituted.with_receiver_placeholder());
537            }
538
539            for parent in &interface.parents {
540                for (name, method_ty) in
541                    self.get_all_methods_recursive(parent, trait_bounds, visited)
542                {
543                    all_interface_methods.insert(name, method_ty);
544                }
545            }
546
547            return all_interface_methods;
548        }
549
550        if let Some(bound_types) = trait_bounds.get(&qualified_name) {
551            return bound_types
552                .iter()
553                .flat_map(|interface_ty| {
554                    self.get_all_methods_recursive(interface_ty, trait_bounds, visited)
555                })
556                .collect();
557        }
558
559        let mut methods = self
560            .get_own_methods(&qualified_name)
561            .cloned()
562            .unwrap_or_default();
563
564        // Type aliases inherit methods from the underlying type.
565        if let Some(definition) = self.get_definition(&qualified_name)
566            && matches!(definition.body, DefinitionBody::TypeAlias { .. })
567        {
568            let alias_ty = &definition.ty;
569            let underlying = match alias_ty {
570                Type::Forall { body, .. } => body.as_ref(),
571                other => other,
572            };
573            let underlying_key = match underlying {
574                Type::Nominal { id, .. } => Some(id.as_str().to_string()),
575                Type::Simple(kind) => Some(format!("prelude.{}", kind.leaf_name())),
576                Type::Compound { kind, .. } => Some(format!("prelude.{}", kind.leaf_name())),
577                _ => None,
578            };
579            // Follow only when the alias body names a different type. For
580            // opaque prelude natives (e.g. `type Map<K, V>`) the body points
581            // to itself — following would loop.
582            if let Some(k) = underlying_key
583                && k != qualified_name.as_str()
584            {
585                let alias_ty = alias_ty.clone();
586                for (name, method_ty) in
587                    self.get_all_methods_recursive(&alias_ty, trait_bounds, visited)
588                {
589                    methods.entry(name).or_insert(method_ty);
590                }
591            }
592        }
593
594        methods
595    }
596
597    pub fn get_methods_from_bounds(
598        &self,
599        qualified_name: &str,
600        trait_bounds: &HashMap<Symbol, Vec<Type>>,
601    ) -> MethodSignatures {
602        if let Some(bound_types) = trait_bounds.get(qualified_name) {
603            return bound_types
604                .iter()
605                .flat_map(|interface_ty| self.get_all_methods(interface_ty, trait_bounds))
606                .collect();
607        }
608        MethodSignatures::default()
609    }
610}
611
612fn domain_display_name(qualified: &str) -> String {
613    let Some((module, name)) = qualified.rsplit_once('.') else {
614        return qualified.to_string();
615    };
616    match module.strip_prefix("go:") {
617        Some(go_module) => {
618            let package = go_module.rsplit('/').next().unwrap_or(go_module);
619            format!("{package}.{name}")
620        }
621        None => name.to_string(),
622    }
623}
624
625/// Return the qualified name used to look up methods/fields for a given type.
626/// For `Type::Compound` and `Type::Simple`, this is the prelude-qualified name
627/// (e.g. `Type::Compound { Slice, .. }` → `"prelude.Slice"`).
628fn method_lookup_key(ty: &Type) -> Option<Symbol> {
629    match ty {
630        Type::Nominal { id, .. } => Some(id.clone()),
631        Type::Compound { kind, .. } => Some(Symbol::from_parts("prelude", kind.leaf_name())),
632        Type::Simple(kind) => Some(Symbol::from_parts("prelude", kind.leaf_name())),
633        _ => None,
634    }
635}
636
637#[cfg(test)]
638mod closed_domain_tests {
639    use super::*;
640    use syntax::ast::StructKind;
641    use syntax::program::{Attributes, TypeAttribute, Visibility};
642
643    fn nominal_int(id: &str) -> Type {
644        Type::Nominal {
645            id: Symbol::from_raw(id),
646            params: vec![],
647            underlying_ty: Some(Box::new(Type::Simple(SimpleKind::Int))),
648        }
649    }
650
651    fn struct_def(ty: Type, closed_domain: bool) -> Definition {
652        let mut attributes = Attributes::default();
653        if closed_domain {
654            attributes.insert(TypeAttribute::ClosedDomain, ());
655        }
656        Definition {
657            visibility: Visibility::Public,
658            ty,
659            name: None,
660            name_span: None,
661            doc: None,
662            body: DefinitionBody::Struct {
663                generics: vec![],
664                fields: vec![],
665                kind: StructKind::Tuple,
666                methods: Default::default(),
667                constructor: None,
668                attributes,
669            },
670        }
671    }
672
673    fn int_const(ty: Type, value: u64) -> Definition {
674        Definition {
675            visibility: Visibility::Public,
676            ty,
677            name: None,
678            name_span: None,
679            doc: None,
680            body: DefinitionBody::Value {
681                allowed_lints: vec![],
682                go_hints: vec![],
683                go_name: None,
684                const_value: Some(Literal::Integer { value, text: None }),
685            },
686        }
687    }
688
689    fn insert(store: &mut Store, module: &str, name: &str, def: Definition) {
690        store.add_module(module);
691        store
692            .get_module_mut(module)
693            .unwrap()
694            .definitions
695            .insert(Symbol::from_raw(name), def);
696    }
697
698    #[test]
699    fn tagged_type_with_members_is_indexed_and_sorted() {
700        let mut store = Store::new();
701        let ty = nominal_int("m.Weekday");
702        insert(&mut store, "m", "m.Weekday", struct_def(ty.clone(), true));
703        insert(&mut store, "m", "m.Saturday", int_const(ty.clone(), 6));
704        insert(&mut store, "m", "m.Sunday", int_const(ty.clone(), 0));
705
706        store.build_closed_domains();
707
708        let domain = store
709            .closed_domains
710            .get("m.Weekday")
711            .expect("tagged type with members should be indexed");
712        assert_eq!(domain.base, SimpleKind::Int);
713        assert_eq!(domain.type_display.as_str(), "Weekday");
714        let names: Vec<&str> = domain
715            .members
716            .iter()
717            .map(|m| m.display_name.as_str())
718            .collect();
719        assert_eq!(names, vec!["Sunday", "Saturday"]);
720    }
721
722    #[test]
723    fn untagged_type_is_absent() {
724        let mut store = Store::new();
725        let ty = nominal_int("m.Plain");
726        insert(&mut store, "m", "m.Plain", struct_def(ty.clone(), false));
727        insert(&mut store, "m", "m.One", int_const(ty, 1));
728
729        store.build_closed_domains();
730
731        assert!(store.closed_domains.is_empty());
732    }
733
734    #[test]
735    fn tagged_type_without_members_records_no_domain() {
736        let mut store = Store::new();
737        insert(
738            &mut store,
739            "m",
740            "m.Empty",
741            struct_def(nominal_int("m.Empty"), true),
742        );
743
744        store.build_closed_domains();
745
746        assert!(!store.closed_domains.contains_key("m.Empty"));
747    }
748
749    #[test]
750    fn const_in_other_module_does_not_widen_domain() {
751        let mut store = Store::new();
752        let ty = nominal_int("lib.Weekday");
753        insert(
754            &mut store,
755            "lib",
756            "lib.Weekday",
757            struct_def(ty.clone(), true),
758        );
759        insert(&mut store, "lib", "lib.Sunday", int_const(ty.clone(), 0));
760        insert(&mut store, "user", "user.Bad", int_const(ty, 99));
761
762        store.build_closed_domains();
763
764        let domain = store.closed_domains.get("lib.Weekday").unwrap();
765        let names: Vec<&str> = domain
766            .members
767            .iter()
768            .map(|m| m.display_name.as_str())
769            .collect();
770        assert_eq!(names, vec!["Sunday"]);
771    }
772}