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