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