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!(definition.body, DefinitionBody::Enum { .. })
473    }
474
475    pub(crate) fn resolve_type_name(
476        &mut self,
477        store: &Store,
478        type_name: &str,
479    ) -> Option<(String, Type)> {
480        if self.scopes.lookup_type_param(type_name).is_some() {
481            return None;
482        }
483
484        let qualified_name = self.lookup_qualified_name(store, type_name)?;
485        let ty = store.get_type(&qualified_name)?.clone();
486
487        Some((qualified_name, ty))
488    }
489
490    pub(crate) fn resolve_type_from_prelude(
491        &self,
492        store: &Store,
493        type_name: &str,
494    ) -> Option<(String, Type)> {
495        let qualified_name = format!("prelude.{}", type_name);
496        let ty = store.get_type(&qualified_name)?.clone();
497        Some((qualified_name, ty))
498    }
499
500    pub(crate) fn get_all_methods(&self, store: &Store, ty: &Type) -> MethodSignatures {
501        if let Type::Parameter(name) = ty {
502            let trait_bounds = self.scopes.collect_all_trait_bounds();
503            let qualified_name = self.qualify_name(name);
504            return store.get_methods_from_bounds(&qualified_name, &trait_bounds);
505        }
506
507        let resolved = ty.strip_refs().resolve_in(&self.env);
508        let cache_key: EcoString = match &resolved {
509            Type::Nominal { id, .. } => id.as_eco().clone(),
510            Type::Compound { kind, .. } => format!("prelude.{}", kind.leaf_name()).into(),
511            Type::Simple(kind) => format!("prelude.{}", kind.leaf_name()).into(),
512            _ => return MethodSignatures::default(),
513        };
514
515        // Interfaces need type-arg-dependent generic substitution, skip cache.
516        let peeled = store.peel_alias(&resolved);
517        if let Type::Nominal { id: peeled_id, .. } = &peeled
518            && store.get_interface(peeled_id).is_some()
519        {
520            let empty = HashMap::default();
521            return store.get_all_methods(&peeled, &empty);
522        }
523
524        if let Some(cached) = self.method_cache.borrow().get(cache_key.as_str()) {
525            return cached.clone();
526        }
527
528        let empty = HashMap::default();
529        // Pass the env-resolved type so the store's env-less `resolve()` stays
530        // identity-safe: `Type::Var` chains are chased once here rather than
531        // silently returning empty methods in the store.
532        let methods = store.get_all_methods(&resolved, &empty);
533        self.method_cache
534            .borrow_mut()
535            .insert(cache_key, methods.clone());
536        methods
537    }
538
539    pub fn reset_scopes(&mut self) {
540        self.scopes.reset();
541        self.imports.clear();
542    }
543
544    pub(crate) fn with_module_cursor<T>(
545        &mut self,
546        module_id: &str,
547        f: impl FnOnce(&mut Self) -> T,
548    ) -> T {
549        if self.cursor.module_id == module_id {
550            return f(self);
551        }
552
553        let previous_module_id = std::mem::replace(&mut self.cursor.module_id, module_id.into());
554        let result = f(self);
555        self.cursor.module_id = previous_module_id;
556        result
557    }
558
559    pub(crate) fn with_file_context<T>(
560        &mut self,
561        store: &Store,
562        module_id: &str,
563        file_id: u32,
564        imports: &[FileImport],
565        kind: FileContextKind,
566        f: impl FnOnce(&mut Self, &Store) -> T,
567    ) -> T {
568        self.with_module_cursor(module_id, |this| {
569            let saved = this.enter_file_context(store, module_id, file_id, imports, kind);
570            let result = f(this, store);
571            this.exit_file_context(saved);
572            result
573        })
574    }
575
576    pub(crate) fn with_file_context_mut<T>(
577        &mut self,
578        store: &mut Store,
579        module_id: &str,
580        file_id: u32,
581        imports: &[FileImport],
582        kind: FileContextKind,
583        f: impl FnOnce(&mut Self, &mut Store) -> T,
584    ) -> T {
585        self.with_module_cursor(module_id, |this| {
586            let saved = this.enter_file_context(&*store, module_id, file_id, imports, kind);
587            let result = f(this, store);
588            this.exit_file_context(saved);
589            result
590        })
591    }
592
593    fn enter_file_context(
594        &mut self,
595        store: &Store,
596        module_id: &str,
597        file_id: u32,
598        imports: &[FileImport],
599        kind: FileContextKind,
600    ) -> SavedFileContext {
601        let saved = SavedFileContext {
602            file_id: self.cursor.file_id.replace(file_id),
603            scopes: std::mem::take(&mut self.scopes),
604            imports: std::mem::take(&mut self.imports),
605        };
606
607        match kind {
608            FileContextKind::Standard => {
609                self.put_prelude_in_scope(store);
610                self.put_unprefixed_module_in_scope(store, module_id);
611            }
612            FileContextKind::ImportedTypedef => {
613                self.put_prelude_in_scope(store);
614            }
615            FileContextKind::Prelude => {
616                self.put_unprefixed_module_in_scope(store, module_id);
617            }
618        }
619        self.put_imported_modules_in_scope(store, imports);
620
621        saved
622    }
623
624    fn exit_file_context(&mut self, saved: SavedFileContext) {
625        self.scopes = saved.scopes;
626        self.imports = saved.imports;
627        self.cursor.file_id = saved.file_id;
628    }
629
630    pub fn failed(&self) -> bool {
631        self.sink.has_errors()
632    }
633
634    pub fn put_prelude_in_scope(&mut self, store: &Store) {
635        self.put_unprefixed_module_in_scope(store, "prelude");
636        if self.imports.imported_modules.contains_key("prelude") {
637            return;
638        }
639        self.put_module_in_scope(store, "prelude", Some("prelude".to_string()));
640    }
641
642    pub fn put_unprefixed_module_in_scope(&mut self, store: &Store, module_id: &str) {
643        self.put_module_in_scope(store, module_id, None)
644    }
645
646    pub fn put_imported_modules_in_scope(&mut self, store: &Store, imports: &[FileImport]) {
647        let mut seen_aliases: HashMap<String, String> = HashMap::default(); // alias -> path
648        let mut seen_paths: HashSet<String> = HashSet::default();
649
650        for import in imports {
651            if seen_paths.contains(import.name.as_str()) {
652                self.sink.push(diagnostics::infer::duplicate_import_path(
653                    &import.name,
654                    import.name_span,
655                ));
656                continue;
657            }
658            seen_paths.insert(import.name.to_string());
659
660            if matches!(import.alias, Some(ImportAlias::Blank(_))) {
661                continue;
662            }
663
664            if let Some(ImportAlias::Named(alias, alias_span)) = &import.alias
665                && is_reserved_import_alias(alias)
666            {
667                self.sink.push(diagnostics::infer::reserved_import_alias(
668                    alias,
669                    *alias_span,
670                ));
671                continue;
672            }
673
674            let Some(effective) = import.effective_alias(&store.go_package_names) else {
675                continue;
676            };
677
678            if let Some(existing_path) = seen_aliases.get(&effective)
679                && existing_path != &import.name
680            {
681                self.sink.push(diagnostics::infer::import_conflict(
682                    &effective,
683                    existing_path,
684                    &import.name,
685                    import.name_span,
686                ));
687                continue;
688            }
689
690            seen_aliases.insert(effective.clone(), import.name.to_string());
691
692            let module = store.get_module(&import.name);
693            if module.is_none() || module.is_some_and(Module::is_empty_stub) {
694                self.imports.failed_imports.insert(effective);
695                continue;
696            }
697
698            self.put_module_in_scope(store, &import.name, Some(effective));
699        }
700    }
701
702    pub fn put_module_in_scope(&mut self, store: &Store, module_id: &str, prefix: Option<String>) {
703        let Some(prefix) = prefix else {
704            self.imports
705                .unprefixed_imports
706                .insert(module_id.to_string());
707            return;
708        };
709
710        let module = store
711            .get_module(module_id)
712            .expect("module must exist when putting in scope");
713
714        let imported_module_id = module.id.clone();
715        let module_prefix = format!("{}.", module.id);
716
717        let module_struct_fields: Vec<_> = module
718            .definitions
719            .iter()
720            .filter(|(qn, _)| module.is_public(qn))
721            .filter(|(qn, _)| {
722                qn.strip_prefix(&module_prefix)
723                    .is_some_and(|rest| !rest.contains('.'))
724            })
725            .map(|(qn, definition)| {
726                let simple_name = qn
727                    .strip_prefix(&module_prefix)
728                    .expect("qualified_name must start with module prefix");
729                let ty = if let DefinitionBody::Struct {
730                    constructor: Some(ctor_ty),
731                    ..
732                } = &definition.body
733                {
734                    ctor_ty.clone()
735                } else {
736                    definition.ty().clone()
737                };
738                StructFieldDefinition {
739                    doc: None,
740                    attributes: vec![],
741                    visibility: AstVisibility::Public,
742                    name: simple_name.into(),
743                    name_span: Span::dummy(),
744                    annotation: Annotation::Unknown,
745                    ty,
746                }
747            })
748            .collect();
749
750        let ty = Type::ImportNamespace(imported_module_id.clone().into());
751
752        self.imports
753            .imported_modules
754            .insert(prefix.clone(), (module_struct_fields, ty));
755        self.imports
756            .prefix_to_module
757            .insert(prefix, imported_module_id);
758    }
759
760    /// Run a closure speculatively: if it returns `Err`, all type variable
761    /// bindings performed during the closure are rolled back.
762    pub(crate) fn speculatively<T, E>(
763        &mut self,
764        f: impl FnOnce(&mut Self) -> Result<T, E>,
765    ) -> Result<T, E> {
766        let spec = self.env.begin_speculation();
767        let result = f(self);
768        self.env.end_speculation(spec, result.is_err());
769        result
770    }
771}
772
773/// Returns `true` if the given name is reserved and cannot be used as an import alias.
774///
775/// Reserved names include Go keywords, Go predeclared identifiers, Go builtins,
776/// Go type constraint names, and Lisette prelude symbols.
777fn is_reserved_import_alias(name: &str) -> bool {
778    matches!(
779        name,
780        // Go keywords
781        "break"
782        | "case"
783        | "chan"
784        | "const"
785        | "continue"
786        | "default"
787        | "defer"
788        | "else"
789        | "fallthrough"
790        | "for"
791        | "func"
792        | "go"
793        | "goto"
794        | "if"
795        | "interface"
796        | "map"
797        | "package"
798        | "range"
799        | "return"
800        | "select"
801        | "struct"
802        | "switch"
803        | "type"
804        | "var"
805        // Go predeclared identifiers
806        | "nil"
807        | "iota"
808        | "true"
809        | "false"
810        // Go predeclared types
811        | "bool"
812        | "byte"
813        | "complex64"
814        | "complex128"
815        | "error"
816        | "float32"
817        | "float64"
818        | "int"
819        | "int8"
820        | "int16"
821        | "int32"
822        | "int64"
823        | "rune"
824        | "string"
825        | "uint"
826        | "uint8"
827        | "uint16"
828        | "uint32"
829        | "uint64"
830        | "uintptr"
831        // Go builtins
832        | "append"
833        | "cap"
834        | "clear"
835        | "close"
836        | "complex"
837        | "copy"
838        | "delete"
839        | "imag"
840        | "len"
841        | "make"
842        | "max"
843        | "min"
844        | "new"
845        | "panic"
846        | "print"
847        | "println"
848        | "real"
849        | "recover"
850        // Go type constraints
851        | "any"
852        | "comparable"
853        // Special Go identifiers
854        | "init"
855        | "main"
856        // Lisette prelude types and constructors
857        | "Option"
858        | "Result"
859        | "Comparable"
860        | "Ordered"
861        | "Some"
862        | "None"
863        | "Ok"
864        | "Err"
865        // Lisette prelude functions not already covered by Go builtins above
866        | "assert_type"
867        | "imaginary"
868    )
869}