Skip to main content

lisette_semantics/checker/
mod.rs

1pub mod freeze;
2pub mod infer;
3pub(crate) mod registration;
4pub mod scopes;
5pub mod type_env;
6
7use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
8use std::cell::RefCell;
9use std::sync::Arc;
10
11use crate::facts::{BindingIdAllocator, Facts};
12use crate::store::Store;
13use diagnostics::LocalSink;
14use ecow::EcoString;
15use scopes::Scopes;
16use syntax::ast::Visibility as AstVisibility;
17use syntax::ast::{Annotation, Expression, Generic, ImportAlias, Span, StructFieldDefinition};
18use syntax::program::{Definition, DefinitionBody, File, FileImport, MethodSignatures, Module};
19use syntax::types::{SubstitutionMap, Symbol, Type, substitute};
20
21pub use type_env::{EnvResolve, Speculation, TypeEnv, VarState};
22
23#[derive(Debug, Clone)]
24pub struct Cursor {
25    pub module_id: String,
26    pub file_id: Option<u32>,
27}
28
29impl Default for Cursor {
30    fn default() -> Self {
31        Self {
32            module_id: "std".to_string(),
33            file_id: None,
34        }
35    }
36}
37
38impl Cursor {
39    pub fn new() -> Self {
40        Self::default()
41    }
42}
43
44#[derive(Debug, Default)]
45pub struct ImportState {
46    /// Module prefix -> (struct fields, module type)
47    pub imported_modules: HashMap<String, (Vec<StructFieldDefinition>, Type)>,
48    /// Import prefix -> actual module_id in Store (e.g., "http" -> "go:net/http")
49    pub prefix_to_module: HashMap<String, String>,
50    /// Modules whose exports are available without prefix (current module and prelude)
51    pub unprefixed_imports: HashSet<String>,
52    /// Effective aliases (e.g. `mux`) of imports whose underlying module
53    /// failed to load (missing typedef, undeclared, module_not_found, etc.).
54    pub failed_imports: HashSet<String>,
55}
56
57impl ImportState {
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    pub fn clear(&mut self) {
63        // Preserve prelude entries since they never change
64        let prelude = self.imported_modules.remove("prelude");
65        self.imported_modules.clear();
66        if let Some(p) = prelude {
67            self.imported_modules.insert("prelude".to_string(), p);
68        }
69        let prelude_mapping = self.prefix_to_module.remove("prelude");
70        self.prefix_to_module.clear();
71        if let Some(m) = prelude_mapping {
72            self.prefix_to_module.insert("prelude".to_string(), m);
73        }
74        self.unprefixed_imports.clear();
75        self.failed_imports.clear();
76    }
77}
78
79#[derive(Debug, Clone, Copy)]
80pub(crate) enum FileContextKind {
81    Standard,
82    ImportedTypedef,
83    Prelude,
84}
85
86struct SavedFileContext {
87    file_id: Option<u32>,
88    scopes: Scopes,
89    imports: ImportState,
90}
91
92/// Cache for builtin types (int, bool, string, etc.) resolved from the prelude.
93/// These never change once populated, so no invalidation needed.
94type BuiltinCache = HashMap<String, Type>;
95
96/// Per-task mutable state. Paired with `AnalysisContext` (shared read-only view).
97pub struct TaskState<'s> {
98    pub env: TypeEnv,
99    pub scopes: Scopes,
100    pub cursor: Cursor,
101    pub imports: ImportState,
102    pub builtins: BuiltinCache,
103    pub sink: &'s LocalSink,
104    pub facts: Facts,
105    /// Recursion guard for interface satisfaction. Prevents
106    /// `collect_interface_violations` from diverging when a bound on `T`
107    /// transitively requires checking `T` against the same interface.
108    pub satisfying_stack: rustc_hash::FxHashSet<(String, String)>,
109    method_cache: RefCell<HashMap<EcoString, MethodSignatures>>,
110    pub ufcs_methods: HashSet<(String, String)>,
111    /// When set, parallel workers read UFCS methods from here instead of cloning into `ufcs_methods`.
112    pub ufcs_shared: Option<Arc<HashSet<(String, String)>>>,
113    /// Typed files produced by inference.
114    pub typed_files: Vec<(String, File)>,
115    /// Reentrancy counter: > 0 while resolving a generic bound annotation.
116    /// Lets `convert_to_type` admit bound-only markers (e.g. `Comparable`)
117    /// without flagging them as misuse in value positions.
118    pub bound_position_depth: u32,
119}
120
121impl<'s> TaskState<'s> {
122    pub fn new(sink: &'s LocalSink, binding_ids: Arc<BindingIdAllocator>) -> Self {
123        Self {
124            env: TypeEnv::new(),
125            scopes: Scopes::new(),
126            cursor: Cursor::new(),
127            imports: ImportState::new(),
128            builtins: BuiltinCache::default(),
129            sink,
130            facts: Facts::new(binding_ids),
131            satisfying_stack: rustc_hash::FxHashSet::default(),
132            method_cache: RefCell::new(HashMap::default()),
133            ufcs_methods: HashSet::default(),
134            ufcs_shared: None,
135            typed_files: Vec::new(),
136            bound_position_depth: 0,
137        }
138    }
139
140    pub fn with_fresh_allocator(sink: &'s LocalSink) -> Self {
141        Self::new(sink, Arc::new(BindingIdAllocator::new()))
142    }
143
144    fn effective_ufcs_methods(&self) -> &HashSet<(String, String)> {
145        self.ufcs_shared.as_deref().unwrap_or(&self.ufcs_methods)
146    }
147
148    pub fn new_type_var(&mut self) -> Type {
149        let id = self.env.fresh(None);
150        Type::Var { id, hint: None }
151    }
152
153    pub fn new_type_var_with_hint(&mut self, hint: &str) -> Type {
154        let hint: EcoString = hint.into();
155        let id = self.env.fresh(Some(hint.clone()));
156        Type::Var {
157            id,
158            hint: Some(hint),
159        }
160    }
161
162    pub fn type_from_literal_expression(&mut self, expression: &Expression) -> Option<Type> {
163        use syntax::ast::{Expression, Literal};
164        match expression {
165            Expression::Literal { literal, .. } => match literal {
166                Literal::Integer { .. } => Some(self.type_int()),
167                Literal::Float { .. } => Some(self.type_float()),
168                Literal::Boolean(_) => Some(self.type_bool()),
169                Literal::String { .. } => Some(self.type_string()),
170                Literal::Char(_) => Some(self.type_char()),
171                _ => None,
172            },
173            Expression::Unary { expression, .. } => self.type_from_literal_expression(expression),
174            _ => None,
175        }
176    }
177
178    pub fn instantiate(&mut self, ty: &Type) -> (Type, SubstitutionMap) {
179        match ty {
180            Type::Forall { vars, body } => {
181                let map: SubstitutionMap = vars
182                    .iter()
183                    .map(|name| {
184                        let id = self.env.fresh(Some(name.clone()));
185                        let fresh_var = Type::Var {
186                            id,
187                            hint: Some(name.clone()),
188                        };
189                        (name.clone(), fresh_var)
190                    })
191                    .collect();
192
193                (substitute(body, &map), map)
194            }
195            _ => (ty.clone(), HashMap::default()),
196        }
197    }
198
199    pub fn new_file_id(&mut self, store: &Store) -> u32 {
200        store.new_file_id()
201    }
202
203    pub fn is_d_lis(&self, store: &Store) -> bool {
204        let Some(file_id) = self.cursor.file_id else {
205            return false;
206        };
207
208        let Some(module) = store.get_module(&self.cursor.module_id) else {
209            return false;
210        };
211
212        module.typedefs.contains_key(&file_id)
213    }
214
215    pub fn is_lis(&self, store: &Store) -> bool {
216        !self.is_d_lis(store)
217    }
218
219    pub(crate) fn current_module<'a>(&self, store: &'a Store) -> &'a Module {
220        store
221            .get_module(&self.cursor.module_id)
222            .expect("current module must exist in store")
223    }
224
225    pub(crate) fn current_module_mut<'a>(&self, store: &'a mut Store) -> &'a mut Module {
226        store
227            .get_module_mut(&self.cursor.module_id)
228            .expect("current module must exist in store")
229    }
230
231    pub(crate) fn qualify_name(&self, name: &str) -> Symbol {
232        Symbol::from_parts(&self.cursor.module_id, name)
233    }
234
235    pub(crate) fn put_in_scope(&mut self, generics: &[Generic]) {
236        for (index, generic) in generics.iter().enumerate() {
237            self.scopes
238                .current_mut()
239                .type_params
240                .get_or_insert_with(HashMap::default)
241                .insert(generic.name.to_string(), index);
242        }
243    }
244
245    /// Validate that all bound annotations on generics refer to types that exist in scope.
246    pub(crate) fn validate_generic_bounds(
247        &mut self,
248        store: &Store,
249        generics: &[Generic],
250        span: &Span,
251    ) {
252        for g in generics {
253            for b in &g.bounds {
254                self.register_bound_annotation(store, b, span);
255            }
256        }
257    }
258
259    pub(crate) fn register_bound_annotation(
260        &mut self,
261        store: &Store,
262        bound: &Annotation,
263        span: &Span,
264    ) -> Type {
265        let resolved = self.convert_bound_to_type(store, bound, span);
266        if self.is_lis(store) && resolved.contains_unknown() {
267            self.sink
268                .push(diagnostics::infer::unknown_in_bound_position(
269                    bound.get_span(),
270                ));
271        }
272        resolved
273    }
274
275    /// Resolve a simple name (e.g., "Sunday") to a public definition in an imported module.
276    /// First tries direct match (`module_id.name`), then falls back to searching
277    /// for nested definitions (e.g., `module_id.Weekday.Sunday`) preferring top-level
278    /// over nested when both share the same simple name.
279    fn resolve_in_imported_module<'m>(
280        &self,
281        module: &'m Module,
282        simple_name: &str,
283    ) -> Option<(String, &'m Definition)> {
284        let module_prefix = format!("{}.", module.id);
285
286        // Direct match: module_id.simple_name
287        let direct = format!("{}{}", module_prefix, simple_name);
288        if let Some(definition) = module.definitions.get(direct.as_str())
289            && definition.visibility().is_public()
290        {
291            return Some((direct, definition));
292        }
293
294        // Nested match: find a public definition whose simple name matches,
295        // e.g., module_id.EnumType.VariantName where simple_name = "VariantName".
296        // Skip if a top-level definition with the same simple name exists
297        // (handles transitive import collisions like go:net/http).
298        let suffix = format!(".{}", simple_name);
299        for (qn, definition) in &module.definitions {
300            if qn.ends_with(suffix.as_str())
301                && qn.starts_with(module_prefix.as_str())
302                && definition.visibility().is_public()
303            {
304                let rest = &qn[module_prefix.len()..];
305                // Only match if it's nested (contains a dot) — direct was tried above
306                if rest.contains('.') {
307                    return Some((qn.to_string(), definition));
308                }
309            }
310        }
311
312        None
313    }
314
315    pub(crate) fn lookup_qualified_name(&self, store: &Store, type_name: &str) -> Option<String> {
316        if let Some((prefix, simple_name)) = type_name.split_once('.')
317            && let Some(module_id) = self.imports.prefix_to_module.get(prefix)
318            && let Some(imported_module) = store.get_module(module_id)
319            && let Some((qualified_name, _)) =
320                self.resolve_in_imported_module(imported_module, simple_name)
321        {
322            return Some(qualified_name);
323        }
324
325        let module = store.get_module(&self.cursor.module_id)?;
326        let qualified_name = Symbol::from_parts(&module.id, type_name);
327
328        if module.definitions.contains_key(qualified_name.as_str()) {
329            return Some(qualified_name.to_string());
330        }
331
332        for imported_module_id in &self.imports.unprefixed_imports {
333            if let Some(imported_module) = store.get_module(imported_module_id) {
334                let qualified_name = Symbol::from_parts(imported_module_id, type_name);
335                if imported_module
336                    .definitions
337                    .contains_key(qualified_name.as_str())
338                {
339                    return Some(qualified_name.to_string());
340                }
341            }
342        }
343
344        None
345    }
346
347    pub(crate) fn get_definition_name_span(
348        &self,
349        store: &Store,
350        qualified_name: &str,
351    ) -> Option<Span> {
352        store.get_definition(qualified_name)?.name_span()
353    }
354
355    pub(crate) fn is_const_name(&self, store: &Store, qualified_name: &str) -> bool {
356        if qualified_name.starts_with("go:") {
357            return false;
358        }
359        store
360            .module_for_qualified_name(qualified_name)
361            .and_then(|module_id| store.get_module(module_id))
362            .is_some_and(|module| module.const_names.contains(qualified_name))
363    }
364
365    pub(crate) fn is_const_var(&self, store: &Store, var_name: &str) -> bool {
366        if self.scopes.lookup_binding_id(var_name).is_some() {
367            return false;
368        }
369        if self.scopes.lookup_const(var_name) {
370            return true;
371        }
372        self.lookup_qualified_name(store, var_name)
373            .is_some_and(|qname| self.is_const_name(store, &qname))
374    }
375
376    /// Track that `name` (at the start of `span`) refers to the definition at `qualified_name`.
377    pub(crate) fn track_name_usage(
378        &mut self,
379        store: &Store,
380        qualified_name: &str,
381        span: &Span,
382        name_len: u32,
383    ) {
384        if let Some(definition_span) = self.get_definition_name_span(store, qualified_name) {
385            let usage_span = Span::new(span.file_id, span.byte_offset, name_len);
386            self.facts.add_usage(usage_span, definition_span);
387        }
388    }
389
390    pub(crate) fn lookup_generic_index(&self, type_name: &str) -> Option<usize> {
391        self.scopes.lookup_type_param(type_name)
392    }
393
394    /// Resolves the value type for a definition. Returns the constructor type for
395    /// structs with constructors (tuple structs) and for type aliases pointing to them.
396    fn resolve_definition_value_type(&self, store: &Store, definition: &Definition) -> Type {
397        if let DefinitionBody::Struct {
398            constructor: Some(ctor_ty),
399            ..
400        } = &definition.body
401        {
402            return ctor_ty.clone();
403        }
404
405        // Type alias to tuple struct should return constructor type.
406        if let DefinitionBody::TypeAlias { .. } = &definition.body {
407            let alias_ty = &definition.ty;
408            let underlying = match alias_ty {
409                Type::Forall { body, .. } => body.as_ref(),
410                other => other,
411            };
412            if let Type::Nominal { id, .. } = underlying
413                && let Some(Definition {
414                    body:
415                        DefinitionBody::Struct {
416                            constructor: Some(ctor_ty),
417                            ..
418                        },
419                    ..
420                }) = store.get_definition(id)
421            {
422                return ctor_ty.clone();
423            }
424        }
425
426        definition.ty().clone()
427    }
428
429    pub(crate) fn lookup_type(&self, store: &Store, value_name: &str) -> Option<Type> {
430        if let Some(ty) = self.scopes.lookup_value(value_name) {
431            return Some(ty.clone());
432        }
433
434        if let Some((_definition, ty)) = self.imports.imported_modules.get(value_name) {
435            return Some(ty.clone());
436        }
437
438        if let Some((prefix, rest)) = value_name.split_once('.')
439            && let Some(module_id) = self.imports.prefix_to_module.get(prefix)
440            && let Some(imported_module) = store.get_module(module_id)
441            && let Some((_, definition)) = self.resolve_in_imported_module(imported_module, rest)
442        {
443            return Some(self.resolve_definition_value_type(store, definition));
444        }
445
446        let module = store.get_module(&self.cursor.module_id)?;
447        let qualified_name = Symbol::from_parts(&module.id, value_name);
448
449        if let Some(definition) = module.definitions.get(qualified_name.as_str()) {
450            return Some(self.resolve_definition_value_type(store, definition));
451        }
452
453        for imported_module_id in &self.imports.unprefixed_imports {
454            if let Some(imported_module) = store.get_module(imported_module_id) {
455                let qualified_name = Symbol::from_parts(imported_module_id, value_name);
456                if let Some(definition) = imported_module.definitions.get(qualified_name.as_str()) {
457                    return Some(self.resolve_definition_value_type(store, definition));
458                }
459            }
460        }
461
462        None
463    }
464
465    pub(crate) fn is_enum_type(&self, store: &Store, ty: &Type) -> bool {
466        let Type::Nominal { id, .. } = ty else {
467            return false;
468        };
469        let Some(definition) = store.get_definition(id) else {
470            return false;
471        };
472        matches!(
473            definition.body,
474            DefinitionBody::Enum { .. } | DefinitionBody::ValueEnum { .. }
475        )
476    }
477
478    pub(crate) fn resolve_type_name(
479        &mut self,
480        store: &Store,
481        type_name: &str,
482    ) -> Option<(String, Type)> {
483        if self.scopes.lookup_type_param(type_name).is_some() {
484            return None;
485        }
486
487        let qualified_name = self.lookup_qualified_name(store, type_name)?;
488        let ty = store.get_type(&qualified_name)?.clone();
489
490        Some((qualified_name, ty))
491    }
492
493    pub(crate) fn resolve_type_from_prelude(
494        &self,
495        store: &Store,
496        type_name: &str,
497    ) -> Option<(String, Type)> {
498        let qualified_name = format!("prelude.{}", type_name);
499        let ty = store.get_type(&qualified_name)?.clone();
500        Some((qualified_name, ty))
501    }
502
503    pub(crate) fn get_all_methods(&self, store: &Store, ty: &Type) -> MethodSignatures {
504        if let Type::Parameter(name) = ty {
505            let trait_bounds = self.scopes.collect_all_trait_bounds();
506            let qualified_name = self.qualify_name(name);
507            return store.get_methods_from_bounds(&qualified_name, &trait_bounds);
508        }
509
510        let resolved = ty.strip_refs().resolve_in(&self.env);
511        let cache_key: EcoString = match &resolved {
512            Type::Nominal { id, .. } => id.as_eco().clone(),
513            Type::Compound { kind, .. } => format!("prelude.{}", kind.leaf_name()).into(),
514            Type::Simple(kind) => format!("prelude.{}", kind.leaf_name()).into(),
515            _ => return MethodSignatures::default(),
516        };
517
518        // Interfaces need type-arg-dependent generic substitution, skip cache.
519        let peeled = store.peel_alias(&resolved);
520        if let Type::Nominal { id: peeled_id, .. } = &peeled
521            && store.get_interface(peeled_id).is_some()
522        {
523            let empty = HashMap::default();
524            return store.get_all_methods(&peeled, &empty);
525        }
526
527        if let Some(cached) = self.method_cache.borrow().get(cache_key.as_str()) {
528            return cached.clone();
529        }
530
531        let empty = HashMap::default();
532        // Pass the env-resolved type so the store's env-less `resolve()` stays
533        // identity-safe: `Type::Var` chains are chased once here rather than
534        // silently returning empty methods in the store.
535        let methods = store.get_all_methods(&resolved, &empty);
536        self.method_cache
537            .borrow_mut()
538            .insert(cache_key, methods.clone());
539        methods
540    }
541
542    pub fn reset_scopes(&mut self) {
543        self.scopes.reset();
544        self.imports.clear();
545    }
546
547    pub(crate) fn with_module_cursor<T>(
548        &mut self,
549        module_id: &str,
550        f: impl FnOnce(&mut Self) -> T,
551    ) -> T {
552        if self.cursor.module_id == module_id {
553            return f(self);
554        }
555
556        let previous_module_id = std::mem::replace(&mut self.cursor.module_id, module_id.into());
557        let result = f(self);
558        self.cursor.module_id = previous_module_id;
559        result
560    }
561
562    pub(crate) fn with_file_context<T>(
563        &mut self,
564        store: &Store,
565        module_id: &str,
566        file_id: u32,
567        imports: &[FileImport],
568        kind: FileContextKind,
569        f: impl FnOnce(&mut Self, &Store) -> T,
570    ) -> T {
571        self.with_module_cursor(module_id, |this| {
572            let saved = this.enter_file_context(store, module_id, file_id, imports, kind);
573            let result = f(this, store);
574            this.exit_file_context(saved);
575            result
576        })
577    }
578
579    pub(crate) fn with_file_context_mut<T>(
580        &mut self,
581        store: &mut Store,
582        module_id: &str,
583        file_id: u32,
584        imports: &[FileImport],
585        kind: FileContextKind,
586        f: impl FnOnce(&mut Self, &mut Store) -> T,
587    ) -> T {
588        self.with_module_cursor(module_id, |this| {
589            let saved = this.enter_file_context(&*store, module_id, file_id, imports, kind);
590            let result = f(this, store);
591            this.exit_file_context(saved);
592            result
593        })
594    }
595
596    fn enter_file_context(
597        &mut self,
598        store: &Store,
599        module_id: &str,
600        file_id: u32,
601        imports: &[FileImport],
602        kind: FileContextKind,
603    ) -> SavedFileContext {
604        let saved = SavedFileContext {
605            file_id: self.cursor.file_id.replace(file_id),
606            scopes: std::mem::take(&mut self.scopes),
607            imports: std::mem::take(&mut self.imports),
608        };
609
610        match kind {
611            FileContextKind::Standard => {
612                self.put_prelude_in_scope(store);
613                self.put_unprefixed_module_in_scope(store, module_id);
614            }
615            FileContextKind::ImportedTypedef => {
616                self.put_prelude_in_scope(store);
617            }
618            FileContextKind::Prelude => {
619                self.put_unprefixed_module_in_scope(store, module_id);
620            }
621        }
622        self.put_imported_modules_in_scope(store, imports);
623
624        saved
625    }
626
627    fn exit_file_context(&mut self, saved: SavedFileContext) {
628        self.scopes = saved.scopes;
629        self.imports = saved.imports;
630        self.cursor.file_id = saved.file_id;
631    }
632
633    pub fn failed(&self) -> bool {
634        self.sink.has_errors()
635    }
636
637    pub fn put_prelude_in_scope(&mut self, store: &Store) {
638        self.put_unprefixed_module_in_scope(store, "prelude");
639        if self.imports.imported_modules.contains_key("prelude") {
640            return;
641        }
642        self.put_module_in_scope(store, "prelude", Some("prelude".to_string()));
643    }
644
645    pub fn put_unprefixed_module_in_scope(&mut self, store: &Store, module_id: &str) {
646        self.put_module_in_scope(store, module_id, None)
647    }
648
649    pub fn put_imported_modules_in_scope(&mut self, store: &Store, imports: &[FileImport]) {
650        let mut seen_aliases: HashMap<String, String> = HashMap::default(); // alias -> path
651        let mut seen_paths: HashSet<String> = HashSet::default();
652
653        for import in imports {
654            if seen_paths.contains(import.name.as_str()) {
655                self.sink.push(diagnostics::infer::duplicate_import_path(
656                    &import.name,
657                    import.name_span,
658                ));
659                continue;
660            }
661            seen_paths.insert(import.name.to_string());
662
663            if matches!(import.alias, Some(ImportAlias::Blank(_))) {
664                continue;
665            }
666
667            if let Some(ImportAlias::Named(alias, alias_span)) = &import.alias
668                && is_reserved_import_alias(alias)
669            {
670                self.sink.push(diagnostics::infer::reserved_import_alias(
671                    alias,
672                    *alias_span,
673                ));
674                continue;
675            }
676
677            let Some(effective) = import.effective_alias(&store.go_package_names) else {
678                continue;
679            };
680
681            if let Some(existing_path) = seen_aliases.get(&effective)
682                && existing_path != &import.name
683            {
684                self.sink.push(diagnostics::infer::import_conflict(
685                    &effective,
686                    existing_path,
687                    &import.name,
688                    import.name_span,
689                ));
690                continue;
691            }
692
693            seen_aliases.insert(effective.clone(), import.name.to_string());
694
695            let module = store.get_module(&import.name);
696            if module.is_none() || module.is_some_and(Module::is_empty_stub) {
697                self.imports.failed_imports.insert(effective);
698                continue;
699            }
700
701            self.put_module_in_scope(store, &import.name, Some(effective));
702        }
703    }
704
705    pub fn put_module_in_scope(&mut self, store: &Store, module_id: &str, prefix: Option<String>) {
706        let Some(prefix) = prefix else {
707            self.imports
708                .unprefixed_imports
709                .insert(module_id.to_string());
710            return;
711        };
712
713        let module = store
714            .get_module(module_id)
715            .expect("module must exist when putting in scope");
716
717        let imported_module_id = module.id.clone();
718        let module_prefix = format!("{}.", module.id);
719
720        let module_struct_fields: Vec<_> = module
721            .definitions
722            .iter()
723            .filter(|(qn, _)| module.is_public(qn))
724            .filter(|(qn, _)| {
725                qn.strip_prefix(&module_prefix)
726                    .is_some_and(|rest| !rest.contains('.'))
727            })
728            .map(|(qn, definition)| {
729                let simple_name = qn
730                    .strip_prefix(&module_prefix)
731                    .expect("qualified_name must start with module prefix");
732                let ty = if let DefinitionBody::Struct {
733                    constructor: Some(ctor_ty),
734                    ..
735                } = &definition.body
736                {
737                    ctor_ty.clone()
738                } else {
739                    definition.ty().clone()
740                };
741                StructFieldDefinition {
742                    doc: None,
743                    attributes: vec![],
744                    visibility: AstVisibility::Public,
745                    name: simple_name.into(),
746                    name_span: Span::dummy(),
747                    annotation: Annotation::Unknown,
748                    ty,
749                }
750            })
751            .collect();
752
753        let ty = Type::ImportNamespace(imported_module_id.clone().into());
754
755        self.imports
756            .imported_modules
757            .insert(prefix.clone(), (module_struct_fields, ty));
758        self.imports
759            .prefix_to_module
760            .insert(prefix, imported_module_id);
761    }
762
763    /// Run a closure speculatively: if it returns `Err`, all type variable
764    /// bindings performed during the closure are rolled back.
765    pub(crate) fn speculatively<T, E>(
766        &mut self,
767        f: impl FnOnce(&mut Self) -> Result<T, E>,
768    ) -> Result<T, E> {
769        let spec = self.env.begin_speculation();
770        let result = f(self);
771        self.env.end_speculation(spec, result.is_err());
772        result
773    }
774}
775
776/// Returns `true` if the given name is reserved and cannot be used as an import alias.
777///
778/// Reserved names include Go keywords, Go predeclared identifiers, Go builtins,
779/// Go type constraint names, and Lisette prelude symbols.
780fn is_reserved_import_alias(name: &str) -> bool {
781    matches!(
782        name,
783        // Go keywords
784        "break"
785        | "case"
786        | "chan"
787        | "const"
788        | "continue"
789        | "default"
790        | "defer"
791        | "else"
792        | "fallthrough"
793        | "for"
794        | "func"
795        | "go"
796        | "goto"
797        | "if"
798        | "interface"
799        | "map"
800        | "package"
801        | "range"
802        | "return"
803        | "select"
804        | "struct"
805        | "switch"
806        | "type"
807        | "var"
808        // Go predeclared identifiers
809        | "nil"
810        | "iota"
811        | "true"
812        | "false"
813        // Go predeclared types
814        | "bool"
815        | "byte"
816        | "complex64"
817        | "complex128"
818        | "error"
819        | "float32"
820        | "float64"
821        | "int"
822        | "int8"
823        | "int16"
824        | "int32"
825        | "int64"
826        | "rune"
827        | "string"
828        | "uint"
829        | "uint8"
830        | "uint16"
831        | "uint32"
832        | "uint64"
833        | "uintptr"
834        // Go builtins
835        | "append"
836        | "cap"
837        | "clear"
838        | "close"
839        | "complex"
840        | "copy"
841        | "delete"
842        | "imag"
843        | "len"
844        | "make"
845        | "max"
846        | "min"
847        | "new"
848        | "panic"
849        | "print"
850        | "println"
851        | "real"
852        | "recover"
853        // Go type constraints
854        | "any"
855        | "comparable"
856        // Special Go identifiers
857        | "init"
858        | "main"
859        // Lisette prelude types and constructors
860        | "Option"
861        | "Result"
862        | "Comparable"
863        | "Ordered"
864        | "Some"
865        | "None"
866        | "Ok"
867        | "Err"
868        // Lisette prelude functions not already covered by Go builtins above
869        | "assert_type"
870        | "imaginary"
871    )
872}