Skip to main content

lisette_semantics/checker/
mod.rs

1pub mod infer;
2mod registration;
3pub mod scopes;
4
5use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
6use std::cell::RefCell;
7use std::rc::Rc;
8
9use crate::facts::Facts;
10use crate::store::Store;
11use diagnostics::DiagnosticSink;
12use ecow::EcoString;
13use scopes::Scopes;
14use syntax::ast::Visibility as AstVisibility;
15use syntax::ast::{Annotation, Expression, Generic, ImportAlias, Span, StructFieldDefinition};
16use syntax::program::{
17    CoercionInfo, Definition, FileImport, MethodSignatures, Module, ResolutionInfo,
18};
19use syntax::types::{SubstitutionMap, Type, TypeVariableState, substitute};
20
21#[derive(Debug, Default)]
22pub struct IdGen {
23    next_type_var_id: i32,
24}
25
26impl IdGen {
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    pub fn new_type_var_id(&mut self) -> i32 {
32        let id = self.next_type_var_id;
33        self.next_type_var_id += 1;
34        id
35    }
36}
37
38#[derive(Debug, Clone)]
39pub struct Cursor {
40    pub module_id: String,
41    pub file_id: Option<u32>,
42}
43
44impl Default for Cursor {
45    fn default() -> Self {
46        Self {
47            module_id: "std".to_string(),
48            file_id: None,
49        }
50    }
51}
52
53impl Cursor {
54    pub fn new() -> Self {
55        Self::default()
56    }
57}
58
59#[derive(Debug, Default)]
60pub struct ImportState {
61    /// Module prefix -> (struct fields, module type)
62    pub imported_modules: HashMap<String, (Vec<StructFieldDefinition>, Type)>,
63    /// Import prefix -> actual module_id in Store (e.g., "http" -> "go:net/http")
64    pub prefix_to_module: HashMap<String, String>,
65    /// Modules whose exports are available without prefix (current module and prelude)
66    pub unprefixed_imports: HashSet<String>,
67}
68
69impl ImportState {
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    pub fn clear(&mut self) {
75        // Preserve prelude entries since they never change
76        let prelude = self.imported_modules.remove("prelude");
77        self.imported_modules.clear();
78        if let Some(p) = prelude {
79            self.imported_modules.insert("prelude".to_string(), p);
80        }
81        let prelude_mapping = self.prefix_to_module.remove("prelude");
82        self.prefix_to_module.clear();
83        if let Some(m) = prelude_mapping {
84            self.prefix_to_module.insert("prelude".to_string(), m);
85        }
86        self.unprefixed_imports.clear();
87    }
88}
89
90/// Cache for builtin types (int, bool, string, etc.) resolved from the prelude.
91/// These never change once populated, so no invalidation needed.
92type BuiltinCache = HashMap<String, Type>;
93
94pub(crate) struct InferenceState {
95    pub type_param_depth: u32,
96    pub satisfying_stack: rustc_hash::FxHashSet<(String, String)>,
97    pub inferring_assignment_target: bool,
98    pub impl_receiver_type: Option<Type>,
99    pub undo_log: Option<Vec<(Rc<RefCell<TypeVariableState>>, TypeVariableState)>>,
100    /// True when inside a match/select arm body. Used to determine whether
101    /// break/continue need Go labels (since Go switch cases don't fall through).
102    pub in_match_arm: bool,
103    /// One entry per enclosing loop; set to `true` when break/continue is
104    /// encountered inside a match arm (i.e. `in_match_arm` is true).
105    pub loop_needs_label_stack: Vec<bool>,
106    /// True when we are inside a compound expression (call arg, binary operand,
107    /// etc.).  Used to reject `Err(x)?`/`None?` in value positions where they
108    /// can never produce a value.
109    pub in_subexpression: bool,
110}
111
112impl InferenceState {
113    pub fn new() -> Self {
114        Self {
115            type_param_depth: 0,
116            satisfying_stack: rustc_hash::FxHashSet::default(),
117            inferring_assignment_target: false,
118            impl_receiver_type: None,
119            undo_log: None,
120            in_match_arm: false,
121            loop_needs_label_stack: Vec::new(),
122            in_subexpression: false,
123        }
124    }
125}
126
127/// A check to run after inference completes for a file.
128pub enum PostInferenceCheck {
129    /// Generic call where type args couldn't be inferred (e.g., `Ok(42)` without context)
130    GenericCall { return_ty: Type, span: Span },
131    /// Empty collection binding where element type couldn't be inferred (e.g., `let x = []`)
132    EmptyCollection { name: String, ty: Type, span: Span },
133    /// Statement-only tail where expected type was a variable at check time
134    StatementTail { expected_ty: Type, span: Span },
135}
136
137pub struct Checker<'r, 's> {
138    pub ids: IdGen,
139    pub store: &'r mut Store,
140    pub scopes: Scopes,
141    pub cursor: Cursor,
142    pub imports: ImportState,
143    pub builtins: BuiltinCache,
144    pub sink: &'s DiagnosticSink,
145    pub facts: Facts,
146    pub coercions: CoercionInfo,
147    pub resolutions: ResolutionInfo,
148    pub post_inference_checks: Vec<PostInferenceCheck>,
149    pub(crate) inference: InferenceState,
150    method_cache: RefCell<HashMap<EcoString, MethodSignatures>>,
151    pub ufcs_methods: HashSet<(String, String)>,
152}
153
154impl<'r, 's> Checker<'r, 's> {
155    pub fn new(store: &'r mut Store, sink: &'s DiagnosticSink) -> Self {
156        Self {
157            ids: IdGen::new(),
158            store,
159            scopes: Scopes::new(),
160            cursor: Cursor::new(),
161            imports: ImportState::new(),
162            builtins: BuiltinCache::default(),
163            sink,
164            facts: Facts::new(),
165            coercions: CoercionInfo::default(),
166            resolutions: ResolutionInfo::default(),
167            post_inference_checks: Vec::new(),
168            inference: InferenceState::new(),
169            method_cache: RefCell::new(HashMap::default()),
170            ufcs_methods: HashSet::default(),
171        }
172    }
173
174    pub fn new_type_var(&mut self) -> Type {
175        let id = self.new_type_var_id();
176        Type::Variable(Rc::new(RefCell::new(TypeVariableState::Unbound {
177            id,
178            hint: None,
179        })))
180    }
181
182    pub fn new_type_var_with_hint(&mut self, hint: &str) -> Type {
183        let id = self.new_type_var_id();
184        Type::Variable(Rc::new(RefCell::new(TypeVariableState::Unbound {
185            id,
186            hint: Some(hint.into()),
187        })))
188    }
189
190    pub fn type_from_literal_expression(&mut self, expression: &Expression) -> Option<Type> {
191        use syntax::ast::{Expression, Literal};
192        match expression {
193            Expression::Literal { literal, .. } => match literal {
194                Literal::Integer { .. } => Some(self.type_int()),
195                Literal::Float { .. } => Some(self.type_float()),
196                Literal::Boolean(_) => Some(self.type_bool()),
197                Literal::String(_) => Some(self.type_string()),
198                Literal::Char(_) => Some(self.type_char()),
199                _ => None,
200            },
201            Expression::Unary { expression, .. } => self.type_from_literal_expression(expression),
202            _ => None,
203        }
204    }
205
206    pub fn instantiate(&mut self, ty: &Type) -> (Type, SubstitutionMap) {
207        match ty {
208            Type::Forall { vars, body } => {
209                let map: SubstitutionMap = vars
210                    .iter()
211                    .map(|name| {
212                        let id = self.new_type_var_id();
213                        let fresh_var =
214                            Type::Variable(Rc::new(RefCell::new(TypeVariableState::Unbound {
215                                id,
216                                hint: Some(name.clone()),
217                            })));
218                        (name.clone(), fresh_var)
219                    })
220                    .collect();
221
222                (substitute(body, &map), map)
223            }
224            _ => (ty.clone(), HashMap::default()),
225        }
226    }
227
228    pub fn new_file_id(&mut self) -> u32 {
229        self.store.new_file_id()
230    }
231
232    pub fn new_type_var_id(&mut self) -> i32 {
233        self.ids.new_type_var_id()
234    }
235
236    pub fn is_d_lis(&self) -> bool {
237        let Some(file_id) = self.cursor.file_id else {
238            return false;
239        };
240
241        let Some(module) = self.store.get_module(&self.cursor.module_id) else {
242            return false;
243        };
244
245        module.typedefs.contains_key(&file_id)
246    }
247
248    pub fn is_lis(&self) -> bool {
249        !self.is_d_lis()
250    }
251
252    pub(crate) fn qualify_name(&self, name: &str) -> String {
253        format!("{}.{}", self.cursor.module_id, name)
254    }
255
256    pub(crate) fn put_in_scope(&mut self, generics: &[Generic]) {
257        for (index, generic) in generics.iter().enumerate() {
258            self.scopes
259                .current_mut()
260                .type_params
261                .get_or_insert_with(HashMap::default)
262                .insert(generic.name.to_string(), index);
263        }
264    }
265
266    /// Resolve a simple name (e.g., "Sunday") to a public definition in an imported module.
267    /// First tries direct match (`module_id.name`), then falls back to searching
268    /// for nested definitions (e.g., `module_id.Weekday.Sunday`) preferring top-level
269    /// over nested when both share the same simple name.
270    fn resolve_in_imported_module<'m>(
271        &self,
272        module: &'m Module,
273        simple_name: &str,
274    ) -> Option<(String, &'m Definition)> {
275        let module_prefix = format!("{}.", module.id);
276
277        // Direct match: module_id.simple_name
278        let direct = format!("{}{}", module_prefix, simple_name);
279        if let Some(definition) = module.definitions.get(direct.as_str())
280            && definition.visibility().is_public()
281        {
282            return Some((direct, definition));
283        }
284
285        // Nested match: find a public definition whose simple name matches,
286        // e.g., module_id.EnumType.VariantName where simple_name = "VariantName".
287        // Skip if a top-level definition with the same simple name exists
288        // (handles transitive import collisions like go:net/http).
289        let suffix = format!(".{}", simple_name);
290        for (qn, definition) in &module.definitions {
291            if qn.ends_with(suffix.as_str())
292                && qn.starts_with(module_prefix.as_str())
293                && definition.visibility().is_public()
294            {
295                let rest = &qn[module_prefix.len()..];
296                // Only match if it's nested (contains a dot) — direct was tried above
297                if rest.contains('.') {
298                    return Some((qn.to_string(), definition));
299                }
300            }
301        }
302
303        None
304    }
305
306    pub(crate) fn lookup_qualified_name(&self, type_name: &str) -> Option<String> {
307        if let Some((prefix, simple_name)) = type_name.split_once('.')
308            && let Some(module_id) = self.imports.prefix_to_module.get(prefix)
309            && let Some(imported_module) = self.store.get_module(module_id)
310            && let Some((qualified_name, _)) =
311                self.resolve_in_imported_module(imported_module, simple_name)
312        {
313            return Some(qualified_name);
314        }
315
316        let module = self.store.get_module(&self.cursor.module_id)?;
317        let qualified_name = format!("{}.{}", module.id, type_name);
318
319        if module.definitions.contains_key(qualified_name.as_str()) {
320            return Some(qualified_name);
321        }
322
323        for imported_module_id in &self.imports.unprefixed_imports {
324            if let Some(imported_module) = self.store.get_module(imported_module_id) {
325                let qualified_name = format!("{}.{}", imported_module_id, type_name);
326                if imported_module
327                    .definitions
328                    .contains_key(qualified_name.as_str())
329                {
330                    return Some(qualified_name);
331                }
332            }
333        }
334
335        None
336    }
337
338    pub(crate) fn get_definition_name_span(&self, qualified_name: &str) -> Option<Span> {
339        self.store.get_definition(qualified_name)?.name_span()
340    }
341
342    /// Track that `name` (at the start of `span`) refers to the definition at `qualified_name`.
343    pub(crate) fn track_name_usage(&mut self, qualified_name: &str, span: &Span, name_len: u32) {
344        if let Some(definition_span) = self.get_definition_name_span(qualified_name) {
345            let usage_span = Span::new(span.file_id, span.byte_offset, name_len);
346            self.facts.add_usage(usage_span, definition_span);
347        }
348    }
349
350    pub(crate) fn lookup_generic_index(&self, type_name: &str) -> Option<usize> {
351        self.scopes.lookup_type_param(type_name)
352    }
353
354    /// Resolves the value type for a definition. Returns the constructor type for
355    /// structs with constructors (tuple structs) and for type aliases pointing to them.
356    fn resolve_definition_value_type(&self, definition: &Definition) -> Type {
357        if let Definition::Struct {
358            constructor: Some(ctor_ty),
359            ..
360        } = definition
361        {
362            return ctor_ty.clone();
363        }
364
365        // Type alias to tuple struct should return constructor type.
366        if let Definition::TypeAlias { ty: alias_ty, .. } = definition {
367            let underlying = match alias_ty {
368                Type::Forall { body, .. } => body.as_ref(),
369                other => other,
370            };
371            if let Type::Constructor { id, .. } = underlying
372                && let Some(Definition::Struct {
373                    constructor: Some(ctor_ty),
374                    ..
375                }) = self.store.get_definition(id)
376            {
377                return ctor_ty.clone();
378            }
379        }
380
381        definition.ty().clone()
382    }
383
384    pub(crate) fn lookup_type(&self, value_name: &str) -> Option<Type> {
385        if let Some(ty) = self.scopes.lookup_value(value_name) {
386            return Some(ty.clone());
387        }
388
389        if let Some((_definition, ty)) = self.imports.imported_modules.get(value_name) {
390            return Some(ty.clone());
391        }
392
393        if let Some((prefix, rest)) = value_name.split_once('.')
394            && let Some(module_id) = self.imports.prefix_to_module.get(prefix)
395            && let Some(imported_module) = self.store.get_module(module_id)
396            && let Some((_, definition)) = self.resolve_in_imported_module(imported_module, rest)
397        {
398            return Some(self.resolve_definition_value_type(definition));
399        }
400
401        let module = self.store.get_module(&self.cursor.module_id)?;
402        let qualified_name = format!("{}.{}", module.id, value_name);
403
404        if let Some(definition) = module.definitions.get(qualified_name.as_str()) {
405            return Some(self.resolve_definition_value_type(definition));
406        }
407
408        for imported_module_id in &self.imports.unprefixed_imports {
409            if let Some(imported_module) = self.store.get_module(imported_module_id) {
410                let qualified_name = format!("{}.{}", imported_module_id, value_name);
411                if let Some(definition) = imported_module.definitions.get(qualified_name.as_str()) {
412                    return Some(self.resolve_definition_value_type(definition));
413                }
414            }
415        }
416
417        None
418    }
419
420    pub(crate) fn is_enum_type(&self, ty: &Type) -> bool {
421        let Type::Constructor { id, .. } = ty else {
422            return false;
423        };
424        let Some(definition) = self.store.get_definition(id) else {
425            return false;
426        };
427        matches!(
428            definition,
429            Definition::Enum { .. } | Definition::ValueEnum { .. }
430        )
431    }
432
433    pub(crate) fn resolve_type_name(&mut self, type_name: &str) -> Option<(String, Type)> {
434        if self.scopes.lookup_type_param(type_name).is_some() {
435            return None;
436        }
437
438        let qualified_name = self.lookup_qualified_name(type_name)?;
439        let ty = self.store.get_type(&qualified_name)?.clone();
440
441        Some((qualified_name, ty))
442    }
443
444    pub(crate) fn resolve_type_from_prelude(&self, type_name: &str) -> Option<(String, Type)> {
445        let qualified_name = format!("prelude.{}", type_name);
446        let ty = self.store.get_type(&qualified_name)?.clone();
447        Some((qualified_name, ty))
448    }
449
450    pub(crate) fn get_all_methods(&self, ty: &Type) -> MethodSignatures {
451        if let Type::Parameter(name) = ty {
452            let trait_bounds = self.scopes.collect_all_trait_bounds();
453            let qualified_name = self.qualify_name(name);
454            return self
455                .store
456                .get_methods_from_bounds(&qualified_name, &trait_bounds);
457        }
458
459        let Type::Constructor { id, .. } = ty.strip_refs().resolve() else {
460            return MethodSignatures::default();
461        };
462
463        // Interfaces need type-arg-dependent generic substitution, skip cache.
464        if self.store.get_interface(&id).is_some() {
465            let empty = HashMap::default();
466            return self.store.get_all_methods(ty, &empty);
467        }
468
469        if let Some(cached) = self.method_cache.borrow().get(&id) {
470            return cached.clone();
471        }
472
473        let empty = HashMap::default();
474        let methods = self.store.get_all_methods(ty, &empty);
475        self.method_cache.borrow_mut().insert(id, methods.clone());
476        methods
477    }
478
479    pub fn reset_scopes(&mut self) {
480        self.scopes.reset();
481        self.imports.clear();
482    }
483
484    pub fn failed(&self) -> bool {
485        self.sink.has_errors()
486    }
487
488    pub fn put_prelude_in_scope(&mut self) {
489        self.put_unprefixed_module_in_scope("prelude");
490        if self.imports.imported_modules.contains_key("prelude") {
491            return;
492        }
493        self.put_module_in_scope("prelude", Some("prelude".to_string()));
494    }
495
496    pub fn put_unprefixed_module_in_scope(&mut self, module_id: &str) {
497        self.put_module_in_scope(module_id, None)
498    }
499
500    pub fn put_imported_modules_in_scope(&mut self, imports: &[FileImport]) {
501        let mut seen_aliases: HashMap<String, String> = HashMap::default(); // alias -> path
502        let mut seen_paths: HashSet<String> = HashSet::default();
503
504        for import in imports {
505            if seen_paths.contains(import.name.as_str()) {
506                self.sink.push(diagnostics::infer::duplicate_import_path(
507                    &import.name,
508                    import.name_span,
509                ));
510                continue;
511            }
512            seen_paths.insert(import.name.to_string());
513
514            if let Some(ImportAlias::Blank(blank_span)) = &import.alias {
515                if !import.name.starts_with("go:") {
516                    self.sink
517                        .push(diagnostics::infer::blank_import_non_go(*blank_span));
518                }
519                continue;
520            }
521
522            if let Some(ImportAlias::Named(alias, alias_span)) = &import.alias
523                && is_reserved_import_alias(alias)
524            {
525                self.sink.push(diagnostics::infer::reserved_import_alias(
526                    alias,
527                    *alias_span,
528                ));
529                continue;
530            }
531
532            let Some(effective) = import.effective_alias() else {
533                continue;
534            };
535
536            if let Some(existing_path) = seen_aliases.get(&effective)
537                && existing_path != &import.name
538            {
539                self.sink.push(diagnostics::infer::import_conflict(
540                    &effective,
541                    existing_path,
542                    &import.name,
543                    import.name_span,
544                ));
545                continue;
546            }
547
548            seen_aliases.insert(effective.clone(), import.name.to_string());
549
550            if !self.store.has(&import.name) {
551                continue;
552            }
553
554            self.put_module_in_scope(&import.name, Some(effective));
555        }
556    }
557
558    pub fn put_module_in_scope(&mut self, module_id: &str, prefix: Option<String>) {
559        let Some(prefix) = prefix else {
560            self.imports
561                .unprefixed_imports
562                .insert(module_id.to_string());
563            return;
564        };
565
566        let module = self
567            .store
568            .get_module(module_id)
569            .expect("module must exist when putting in scope");
570
571        let imported_module_id = module.id.clone();
572        let module_prefix = format!("{}.", module.id);
573
574        let module_struct_fields: Vec<_> = module
575            .definitions
576            .iter()
577            .filter(|(qn, _)| module.is_public(qn))
578            .filter(|(qn, _)| {
579                qn.strip_prefix(&module_prefix)
580                    .is_some_and(|rest| !rest.contains('.'))
581            })
582            .map(|(qn, definition)| {
583                let simple_name = qn
584                    .strip_prefix(&module_prefix)
585                    .expect("qualified_name must start with module prefix");
586                let ty = if let Definition::Struct {
587                    constructor: Some(ctor_ty),
588                    ..
589                } = definition
590                {
591                    ctor_ty.clone()
592                } else {
593                    definition.ty().clone()
594                };
595                StructFieldDefinition {
596                    doc: None,
597                    attributes: vec![],
598                    visibility: AstVisibility::Public,
599                    name: simple_name.into(),
600                    name_span: Span::dummy(),
601                    annotation: Annotation::Unknown,
602                    ty,
603                }
604            })
605            .collect();
606
607        let ty = Type::Constructor {
608            id: format!("@import/{}", imported_module_id).into(),
609            params: vec![],
610            underlying_ty: None,
611        };
612
613        self.imports
614            .imported_modules
615            .insert(prefix.clone(), (module_struct_fields, ty));
616        self.imports
617            .prefix_to_module
618            .insert(prefix, imported_module_id);
619    }
620
621    /// Run a closure speculatively: if it returns `Err`, all type variable
622    /// mutations performed during the closure are rolled back.
623    pub(crate) fn speculatively<T, E>(
624        &mut self,
625        f: impl FnOnce(&mut Self) -> Result<T, E>,
626    ) -> Result<T, E> {
627        let prev_log = self.inference.undo_log.take();
628        self.inference.undo_log = Some(Vec::new());
629        let result = f(self);
630        let log = self.inference.undo_log.take().unwrap();
631        self.inference.undo_log = prev_log;
632        if result.is_err() {
633            for (type_var, original_state) in log.into_iter().rev() {
634                *type_var.borrow_mut() = original_state;
635            }
636        } else if let Some(parent_log) = &mut self.inference.undo_log {
637            parent_log.extend(log);
638        }
639        result
640    }
641
642    pub(crate) fn set_inferring_assignment_target(&mut self) {
643        self.inference.inferring_assignment_target = true;
644    }
645
646    pub(crate) fn clear_inferring_assignment_target(&mut self) {
647        self.inference.inferring_assignment_target = false;
648    }
649
650    pub(crate) fn is_inferring_assignment_target(&self) -> bool {
651        self.inference.inferring_assignment_target
652    }
653}
654
655/// Returns `true` if the given name is reserved and cannot be used as an import alias.
656///
657/// Reserved names include Go keywords, Go predeclared identifiers, Go builtins,
658/// Go type constraint names, and Lisette prelude symbols.
659fn is_reserved_import_alias(name: &str) -> bool {
660    matches!(
661        name,
662        // Go keywords
663        "break"
664        | "case"
665        | "chan"
666        | "const"
667        | "continue"
668        | "default"
669        | "defer"
670        | "else"
671        | "fallthrough"
672        | "for"
673        | "func"
674        | "go"
675        | "goto"
676        | "if"
677        | "interface"
678        | "map"
679        | "package"
680        | "range"
681        | "return"
682        | "select"
683        | "struct"
684        | "switch"
685        | "type"
686        | "var"
687        // Go predeclared identifiers
688        | "nil"
689        | "iota"
690        | "true"
691        | "false"
692        // Go predeclared types
693        | "bool"
694        | "byte"
695        | "complex64"
696        | "complex128"
697        | "error"
698        | "float32"
699        | "float64"
700        | "int"
701        | "int8"
702        | "int16"
703        | "int32"
704        | "int64"
705        | "rune"
706        | "string"
707        | "uint"
708        | "uint8"
709        | "uint16"
710        | "uint32"
711        | "uint64"
712        | "uintptr"
713        // Go builtins
714        | "append"
715        | "cap"
716        | "clear"
717        | "close"
718        | "complex"
719        | "copy"
720        | "delete"
721        | "imag"
722        | "len"
723        | "make"
724        | "max"
725        | "min"
726        | "new"
727        | "panic"
728        | "print"
729        | "println"
730        | "real"
731        | "recover"
732        // Go type constraints
733        | "any"
734        | "comparable"
735        // Special Go identifiers
736        | "init"
737        | "main"
738        // Lisette prelude types and constructors
739        | "Option"
740        | "Result"
741        | "Some"
742        | "None"
743        | "Ok"
744        | "Err"
745    )
746}