Skip to main content

lisette_semantics/checker/infer/
checks.rs

1use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
2
3use diagnostics::UnusedExpressionKind;
4use syntax::ast::{Expression, Generic, Pattern, Span, StructKind, UnaryOperator};
5use syntax::program::{Definition, ReceiverCoercion};
6use syntax::types::{Bound, Type, TypeVariableState};
7
8use crate::checker::PostInferenceCheck;
9
10use super::super::Checker;
11use super::expressions::patterns::collect_pattern_bindings;
12
13impl Checker<'_, '_> {
14    pub(crate) fn check_unused_expression(
15        &mut self,
16        span: Span,
17        ty: &Type,
18        is_literal: bool,
19        allowed_lints: &[String],
20    ) {
21        let kind = if is_literal {
22            Some(UnusedExpressionKind::Literal)
23        } else if ty.is_result() {
24            Some(UnusedExpressionKind::Result)
25        } else if ty.is_option() {
26            Some(UnusedExpressionKind::Option)
27        } else if ty.is_partial() {
28            Some(UnusedExpressionKind::Partial)
29        } else if !ty.is_unit() && !ty.is_variable() && !ty.is_never() {
30            Some(UnusedExpressionKind::Value)
31        } else {
32            None
33        };
34
35        if let Some(kind) = kind
36            && !allowed_lints.contains(&kind.lint_name().to_string())
37        {
38            self.facts.add_unused_expression(span, kind);
39        }
40    }
41
42    pub(crate) fn callee_allowed_lints(
43        &mut self,
44        callee_name: &str,
45        inferred: &Expression,
46    ) -> Vec<String> {
47        if let Some(qualified) = self.lookup_qualified_name(callee_name)
48            && let Some(definition) = self.store.get_definition(&qualified)
49        {
50            return definition.allowed_lints().to_vec();
51        }
52
53        if let Expression::Call { expression, .. } = inferred
54            && let Expression::DotAccess {
55                expression: receiver,
56                member,
57                ..
58            } = expression.as_ref()
59        {
60            let receiver_ty = receiver.get_type().resolve().strip_refs();
61            if let Type::Constructor { id, .. } = &receiver_ty {
62                let method_key = format!("{}.{}", id, member);
63                if let Some(definition) = self.store.get_definition(&method_key) {
64                    return definition.allowed_lints().to_vec();
65                }
66            }
67        }
68
69        vec![]
70    }
71
72    pub(crate) fn get_call_return_type(expression: &Expression) -> Option<Type> {
73        if let Expression::Call { expression, .. } = expression {
74            let callee_ty = expression.get_type().resolve();
75            match callee_ty {
76                Type::Function { return_type, .. } => Some(return_type.as_ref().clone()),
77                _ => None,
78            }
79        } else {
80            None
81        }
82    }
83
84    pub(crate) fn is_channel_send(expression: &Expression) -> bool {
85        if let Expression::Call { expression, .. } = expression
86            && let Expression::DotAccess {
87                expression: receiver,
88                member,
89                ..
90            } = expression.as_ref()
91            && member == "send"
92        {
93            return receiver
94                .get_type()
95                .resolve()
96                .get_name()
97                .map(|n| n == "Channel" || n == "Sender")
98                .unwrap_or(false);
99        }
100        false
101    }
102
103    pub(crate) fn check_unused_type_parameters(&mut self, generics: &[Generic], fn_ty: &Type) {
104        if generics.is_empty() {
105            return;
106        }
107
108        let mut remaining: HashSet<_> = generics.iter().map(|g| g.name.clone()).collect();
109        fn_ty.remove_found_type_names(&mut remaining);
110
111        let is_typedef = self.is_d_lis();
112        for generic in generics {
113            if generic.name.starts_with('_') {
114                continue;
115            }
116
117            if remaining.contains(&generic.name) {
118                self.facts.add_unused_type_param(
119                    generic.name.to_string(),
120                    generic.span,
121                    is_typedef,
122                );
123            }
124        }
125    }
126
127    pub(crate) fn check_prelude_shadowing(&mut self, name: &str, span: Span) {
128        if self.is_d_lis() {
129            return;
130        }
131        let prelude_qualified_name = format!("prelude.{}", name);
132        if let Some(prelude_module) = self.store.get_module("prelude")
133            && prelude_module
134                .definitions
135                .contains_key(prelude_qualified_name.as_str())
136        {
137            self.sink
138                .push(diagnostics::infer::prelude_type_shadowed(name, span));
139        }
140    }
141
142    /// Returns `true` if valid (no error emitted), `false` if an error was emitted.
143    pub(crate) fn check_slice_index_type(
144        &mut self,
145        type_name: &str,
146        index_ty: &Type,
147        span: Span,
148    ) -> bool {
149        if type_name != "Slice" || index_ty.is_variable() {
150            return true;
151        }
152
153        if index_ty.get_name().is_some_and(|n| n == "int") {
154            return true;
155        }
156
157        self.sink
158            .push(diagnostics::infer::slice_index_type_mismatch(
159                index_ty, span,
160            ));
161        false
162    }
163
164    pub(crate) fn check_call_arity(
165        &mut self,
166        param_types: &[Type],
167        args: &[Expression],
168        callee_expression: &Expression,
169        span: &Span,
170    ) {
171        if param_types.len() != args.len() {
172            let actual_types: Vec<Type> = args.iter().map(|e| e.get_type()).collect();
173            let generic_params = self.get_generic_param_names(callee_expression);
174            let is_constructor = callee_expression
175                .get_var_name()
176                .map(|name| name.chars().next().is_some_and(|c| c.is_uppercase()))
177                .unwrap_or(false);
178            self.sink.push(diagnostics::infer::arity_mismatch(
179                param_types,
180                &actual_types,
181                &generic_params,
182                is_constructor,
183                *span,
184            ));
185        }
186    }
187
188    fn get_generic_param_names(&self, expression: &Expression) -> Vec<String> {
189        if let Expression::Identifier { value, .. } = expression
190            && let Some(ty) = self.scopes.lookup_value(value)
191        {
192            return match ty {
193                Type::Forall { vars, .. } => vars.iter().map(|s| s.to_string()).collect(),
194                _ => vec![],
195            };
196        }
197        vec![]
198    }
199
200    pub(crate) fn check_unconstrained_bounded_type_params(
201        &mut self,
202        bounds: &[Bound],
203        span: &Span,
204    ) {
205        for bound in bounds {
206            if let Type::Variable(var) = &bound.generic.resolve()
207                && let TypeVariableState::Unbound { .. } = &*var.borrow()
208            {
209                self.sink.push(diagnostics::infer::unconstrained_type_param(
210                    &bound.param_name,
211                    *span,
212                ));
213            }
214        }
215    }
216
217    /// Runs all post-inference checks for unresolved type variables.
218    /// Called after inference completes for a file, when type variables have had
219    /// a chance to be constrained by later usage.
220    pub fn run_post_inference_checks(&mut self) {
221        for check in std::mem::take(&mut self.post_inference_checks) {
222            match check {
223                PostInferenceCheck::GenericCall { return_ty, span } => {
224                    if return_ty.resolve().has_unbound_variables() {
225                        self.sink
226                            .push(diagnostics::infer::cannot_infer_type_argument(span));
227                    }
228                }
229                PostInferenceCheck::EmptyCollection { name, ty, span } => {
230                    if ty.resolve().has_unbound_variables() {
231                        self.sink
232                            .push(diagnostics::infer::uninferred_binding(&name, span));
233                    }
234                }
235                PostInferenceCheck::StatementTail { expected_ty, span } => {
236                    let resolved = expected_ty.resolve();
237                    if !resolved.is_unit()
238                        && !matches!(resolved, Type::Variable(_))
239                        && !expected_ty.is_ignored()
240                    {
241                        self.sink.push(diagnostics::infer::statement_as_tail(span));
242                    }
243                }
244            }
245        }
246    }
247
248    pub(crate) fn check_constrained_return_type(
249        &mut self,
250        return_ty: &Type,
251        generics: &[Generic],
252        span: &Span,
253        fn_name: &str,
254    ) {
255        let Type::Constructor { id, params, .. } = return_ty else {
256            return;
257        };
258
259        if params.is_empty() {
260            return;
261        }
262
263        let qualified_id = self
264            .lookup_qualified_name(id)
265            .unwrap_or_else(|| id.as_ref().to_string());
266        let Some(definition) = self.store.get_definition(&qualified_id) else {
267            return;
268        };
269
270        let methods = match definition {
271            syntax::program::Definition::Struct { methods, .. } => methods,
272            syntax::program::Definition::Enum { methods, .. } => methods,
273            _ => return,
274        };
275
276        let mut required_bounds: HashMap<String, Vec<Type>> = HashMap::default();
277
278        for method_ty in methods.values() {
279            if let Type::Forall { vars: _, body } = method_ty {
280                if let Type::Function { bounds, .. } = body.as_ref() {
281                    for bound in bounds {
282                        if let Type::Parameter(param_name) = &bound.generic {
283                            let entry = required_bounds
284                                .entry(param_name.as_ref().to_string())
285                                .or_default();
286                            if !entry.contains(&bound.ty) {
287                                entry.push(bound.ty.clone());
288                            }
289                        }
290                    }
291                }
292            } else if let Type::Function { bounds, .. } = method_ty {
293                for bound in bounds {
294                    if let Type::Parameter(param_name) = &bound.generic {
295                        let entry = required_bounds
296                            .entry(param_name.as_ref().to_string())
297                            .or_default();
298                        if !entry.contains(&bound.ty) {
299                            entry.push(bound.ty.clone());
300                        }
301                    }
302                }
303            }
304        }
305
306        for return_param in params.iter() {
307            if let Type::Parameter(param_name) = return_param
308                && let Some(method_bounds) = required_bounds.get(param_name.as_ref())
309            {
310                let fn_generic = generics
311                    .iter()
312                    .find(|g| g.name.as_ref() == param_name.as_ref());
313
314                if let Some(fn_gen) = fn_generic {
315                    let fn_bounds: Vec<Type> = fn_gen
316                        .bounds
317                        .iter()
318                        .map(|b| self.convert_to_type(b, span))
319                        .collect();
320
321                    for method_bound in method_bounds {
322                        if !fn_bounds.iter().any(|fb| fb == method_bound) {
323                            self.sink.push(
324                                diagnostics::infer::missing_constraint_on_generic_return_type(
325                                    fn_name,
326                                    param_name.as_ref(),
327                                    method_bound,
328                                    *span,
329                                ),
330                            );
331                        }
332                    }
333                } else {
334                    // Generic parameter not declared on function — check impl-level bounds
335                    let scope_bounds = self.scopes.collect_all_trait_bounds();
336                    let qualified = self.qualify_name(param_name.as_ref());
337                    let impl_bounds = scope_bounds
338                        .get(qualified.as_str())
339                        .or_else(|| scope_bounds.get(param_name.as_ref()));
340
341                    let all_covered = impl_bounds
342                        .is_some_and(|ib| method_bounds.iter().all(|mb| ib.contains(mb)));
343
344                    if !all_covered {
345                        let bound_str = method_bounds
346                            .iter()
347                            .map(|b| b.to_string())
348                            .collect::<Vec<_>>()
349                            .join(" + ");
350
351                        self.sink.push(
352                            diagnostics::infer::missing_constraint_on_generic_return_type(
353                                fn_name,
354                                param_name.as_ref(),
355                                &Type::Parameter(bound_str.into()),
356                                *span,
357                            ),
358                        );
359                    }
360                }
361            }
362        }
363    }
364
365    /// Checks whether an assignment target contains `.0` on a newtype
366    /// (single-field, non-generic tuple struct). Newtypes compile to Go named
367    /// types, so `.0` is read-only; users must reconstruct instead.
368    pub(crate) fn check_newtype_field_assignment(&mut self, target: &Expression, span: Span) {
369        match target {
370            Expression::DotAccess {
371                expression, member, ..
372            } => {
373                if member == "0" {
374                    let base_ty = expression.get_type().resolve();
375                    let ty = base_ty.strip_refs();
376                    if let Type::Constructor { id, params, .. } = ty
377                        && params.is_empty()
378                        && let Some(Definition::Struct {
379                            kind: StructKind::Tuple,
380                            fields,
381                            generics,
382                            ..
383                        }) = self.store.get_definition(id.as_str())
384                        && fields.len() == 1
385                        && generics.is_empty()
386                    {
387                        let type_name = id.rsplit('.').next().unwrap_or(id.as_str());
388                        self.sink.push(diagnostics::infer::newtype_field_assignment(
389                            type_name, span,
390                        ));
391                        return;
392                    }
393                }
394                // Recurse into inner DotAccess to catch chains like w.0.x = 2
395                self.check_newtype_field_assignment(expression, span);
396            }
397            Expression::IndexedAccess { expression, .. } => {
398                self.check_newtype_field_assignment(expression, span);
399            }
400            Expression::Unary {
401                operator: UnaryOperator::Deref,
402                expression,
403                ..
404            } => {
405                self.check_newtype_field_assignment(expression, span);
406            }
407            _ => {}
408        }
409    }
410
411    /// Checks whether an assignment target has a DotAccess above a Map IndexedAccess.
412    /// Map entries are returned by copy in Go, so `m["key"].field = v` is invalid.
413    /// Users must extract, modify, and reinsert.
414    pub(crate) fn check_map_field_chain_assignment(&mut self, target: &Expression, span: Span) {
415        if self.has_map_field_in_chain(target) {
416            self.sink
417                .push(diagnostics::infer::map_field_chain_assignment(span));
418        }
419    }
420
421    pub(crate) fn has_map_field_in_chain(&self, expression: &Expression) -> bool {
422        match expression.unwrap_parens() {
423            Expression::DotAccess { expression, .. } => {
424                self.is_map_indexed_access(expression) || self.has_map_field_in_chain(expression)
425            }
426            _ => false,
427        }
428    }
429
430    fn is_map_indexed_access(&self, expression: &Expression) -> bool {
431        match expression.unwrap_parens() {
432            Expression::IndexedAccess { expression, .. } => {
433                expression.get_type().resolve().has_name("Map")
434            }
435            _ => false,
436        }
437    }
438
439    /// Check if an identifier used as a value (not in call position) refers to a
440    /// native method, native constructor, or private method expression.
441    pub(crate) fn check_native_value_usage(&mut self, value: &str, ty: &Type, span: Span) {
442        if matches!(
443            value,
444            "imaginary" | "assert_type" | "complex" | "real" | "panic"
445        ) {
446            let qualified = format!("{}.{}", self.cursor.module_id, value);
447            if self.store.get_definition(&qualified).is_none() {
448                self.sink
449                    .push(diagnostics::infer::native_constructor_value(value, span));
450                return;
451            }
452        }
453
454        {
455            let qualified = if value.contains('.') {
456                value.to_string()
457            } else {
458                format!("{}.{}", self.cursor.module_id, value)
459            };
460            let is_tuple_struct = match self.store.get_definition(&qualified) {
461                Some(Definition::Struct {
462                    kind: StructKind::Tuple,
463                    ..
464                }) => true,
465                Some(Definition::TypeAlias { ty: alias_ty, .. }) => {
466                    if let Type::Constructor { id, .. } = alias_ty.unwrap_forall() {
467                        matches!(
468                            self.store.get_definition(id),
469                            Some(Definition::Struct {
470                                kind: StructKind::Tuple,
471                                ..
472                            })
473                        )
474                    } else {
475                        false
476                    }
477                }
478                _ => false,
479            };
480            if is_tuple_struct {
481                self.sink
482                    .push(diagnostics::infer::native_constructor_value(value, span));
483                return;
484            }
485        }
486
487        let Some((type_part, method_part)) = value.split_once('.') else {
488            return;
489        };
490        if method_part.contains('.') {
491            return;
492        }
493
494        let is_native = matches!(
495            type_part,
496            "Slice" | "EnumeratedSlice" | "Map" | "Channel" | "Sender" | "Receiver" | "string"
497        );
498
499        if is_native {
500            if matches!(method_part, "new" | "buffered") {
501                self.sink
502                    .push(diagnostics::infer::native_constructor_value(value, span));
503            } else {
504                self.sink
505                    .push(diagnostics::infer::native_method_value(method_part, span));
506            }
507            return;
508        }
509
510        if matches!(method_part, "new" | "buffered") {
511            let ret_ty = match ty {
512                Type::Function { return_type, .. } => Some(return_type.as_ref()),
513                Type::Forall { body, .. } => match body.as_ref() {
514                    Type::Function { return_type, .. } => Some(return_type.as_ref()),
515                    _ => None,
516                },
517                _ => None,
518            };
519            if let Some(ret) = ret_ty {
520                let resolved = ret.resolve();
521                let is_native_ret =
522                    matches!(resolved.get_name(), Some("Channel" | "Map" | "Slice"));
523                if is_native_ret {
524                    self.sink
525                        .push(diagnostics::infer::native_constructor_value(value, span));
526                    return;
527                }
528            }
529        }
530
531        let is_fn = matches!(ty, Type::Function { .. } | Type::Forall { .. });
532        if !is_fn {
533            return;
534        }
535        let fn_params = match ty {
536            Type::Function { params, .. } => params.as_slice(),
537            Type::Forall { body, .. } => match body.as_ref() {
538                Type::Function { params, .. } => params.as_slice(),
539                _ => return,
540            },
541            _ => return,
542        };
543        let Some(first) = fn_params.first() else {
544            return;
545        };
546        let stripped = first.strip_refs();
547        let is_self = matches!(&stripped, Type::Constructor { id, .. }
548            if id.rsplit('.').next() == Some(type_part));
549        if !is_self {
550            return;
551        }
552
553        // Look up method visibility
554        let module_id = &self.cursor.module_id;
555        let method_key = format!("{}.{}.{}", module_id, type_part, method_part);
556        let is_public = self
557            .store
558            .get_definition(&method_key)
559            .map(|d| d.visibility().is_public())
560            .unwrap_or(true);
561
562        if !is_public {
563            self.sink
564                .push(diagnostics::infer::private_method_expression(span));
565        }
566    }
567
568    pub(crate) fn has_newtype_dot0_in_chain(&self, expression: &Expression) -> bool {
569        let mut current = expression.unwrap_parens();
570        while let Expression::DotAccess {
571            expression: inner,
572            member,
573            ..
574        } = current
575        {
576            if member.parse::<usize>().is_ok() {
577                let ty = inner.get_type().resolve().strip_refs();
578                if let Type::Constructor { id, .. } = &ty
579                    && let Some(Definition::Struct {
580                        kind: StructKind::Tuple,
581                        fields,
582                        generics,
583                        ..
584                    }) = self.store.get_definition(id.as_str())
585                    && fields.len() == 1
586                    && generics.is_empty()
587                {
588                    return true;
589                }
590            }
591            current = inner.unwrap_parens();
592        }
593        false
594    }
595
596    pub(crate) fn check_not_temp_producing(&mut self, expression: &Expression) {
597        if is_temp_producing(expression) || self.has_auto_address_on_call(expression) {
598            self.sink.push(diagnostics::infer::complex_sub_expression(
599                expression.get_span(),
600            ));
601        }
602    }
603
604    /// Check if this expression will produce pre-statements at emit time
605    /// due to auto-address coercion on a function call result receiver.
606    /// E.g. `make_box().get()` where `get` takes `Ref<Box>` — the emitter
607    /// must hoist `make_box()` into a temp to take its address.
608    fn has_auto_address_on_call(&self, expression: &Expression) -> bool {
609        let expression = expression.unwrap_parens();
610        if let Expression::Call { expression, .. } = expression
611            && let Expression::DotAccess {
612                expression: receiver,
613                ..
614            } = expression.unwrap_parens()
615        {
616            if matches!(receiver.unwrap_parens(), Expression::Call { .. })
617                && self.coercions.get_coercion(receiver.get_span())
618                    == Some(ReceiverCoercion::AutoAddress)
619            {
620                return true;
621            }
622            return self.has_auto_address_on_call(receiver);
623        }
624        false
625    }
626}
627
628pub(crate) fn is_temp_producing(expression: &Expression) -> bool {
629    matches!(
630        expression.unwrap_parens(),
631        Expression::If { .. }
632            | Expression::IfLet { .. }
633            | Expression::Match { .. }
634            | Expression::Block { .. }
635            | Expression::Loop { .. }
636            | Expression::Select { .. }
637            | Expression::TryBlock { .. }
638            | Expression::RecoverBlock { .. }
639    )
640}
641
642pub(crate) fn check_is_non_addressable(expression: &Expression) -> Option<&'static str> {
643    match expression {
644        Expression::Identifier { .. } => None,
645        Expression::DotAccess { expression, .. } => {
646            let inner = expression.unwrap_parens();
647            // Allow &call().x when call returns Ref<T> — pointer fields are addressable.
648            let is_non_addressable_origin = matches!(inner, Expression::StructCall { .. })
649                || (matches!(inner, Expression::Call { .. })
650                    && !expression.get_type().resolve().is_ref());
651            if is_non_addressable_origin {
652                Some("field access on non-addressable value")
653            } else {
654                check_is_non_addressable(expression)
655            }
656        }
657        Expression::IndexedAccess { expression, .. } => {
658            let expression_ty = expression.get_type().resolve();
659            if let Some(name) = expression_ty.get_name() {
660                if name == "Map" {
661                    return Some("map index expression");
662                }
663                // Slice elements are always addressable, even from call results.
664                if name == "Slice" {
665                    return None;
666                }
667            }
668            if matches!(expression.unwrap_parens(), Expression::Call { .. }) {
669                Some("index access on function call")
670            } else {
671                check_is_non_addressable(expression)
672            }
673        }
674        Expression::Unary {
675            operator: UnaryOperator::Deref,
676            ..
677        } => None,
678        Expression::StructCall { .. } => None,
679        Expression::Paren { expression, .. } => check_is_non_addressable(expression),
680        Expression::Call { .. } => None,
681        Expression::Literal { .. } => Some("literal"),
682        Expression::Binary { .. } => Some("binary expression"),
683        Expression::If { .. } | Expression::IfLet { .. } => Some("conditional expression"),
684        Expression::Match { .. } => Some("match expression"),
685        Expression::Block { .. } => Some("block expression"),
686        Expression::Lambda { .. } => Some("lambda"),
687        Expression::Tuple { .. } => Some("tuple"),
688        Expression::Range { .. } => Some("range expression"),
689        _ => Some("expression"),
690    }
691}
692
693/// Check if an assignment target roots at a non-addressable expression.
694/// Walks DotAccess/IndexedAccess chains to find the root. Call results,
695/// struct literals, and tuple literals are not valid assignment roots.
696pub(crate) fn check_non_addressable_assignment_target(
697    expression: &Expression,
698) -> Option<&'static str> {
699    match expression.unwrap_parens() {
700        Expression::Identifier { .. } => None,
701        Expression::DotAccess { expression, .. } => {
702            // Allow assignment through pointer: make().x = 5 when make() -> Ref<T>
703            if matches!(expression.unwrap_parens(), Expression::Call { .. })
704                && expression.get_type().resolve().is_ref()
705            {
706                None
707            } else {
708                check_non_addressable_assignment_target(expression)
709            }
710        }
711        Expression::IndexedAccess { .. } => None,
712        Expression::Unary {
713            operator: UnaryOperator::Deref,
714            ..
715        } => None,
716        Expression::Call { .. } => Some("function call result"),
717        Expression::StructCall { .. } => Some("struct literal"),
718        Expression::Tuple { .. } => Some("tuple literal"),
719        _ => None,
720    }
721}
722
723pub(crate) fn check_duplicate_bindings(sink: &diagnostics::DiagnosticSink, pattern: &Pattern) {
724    if let Pattern::Or { patterns, .. } = pattern {
725        for alternative_pattern in patterns {
726            check_duplicate_bindings(sink, alternative_pattern);
727        }
728        return;
729    }
730
731    if matches!(
732        pattern,
733        Pattern::Identifier { .. }
734            | Pattern::WildCard { .. }
735            | Pattern::Literal { .. }
736            | Pattern::Unit { .. }
737    ) {
738        return;
739    }
740
741    let bindings = collect_pattern_bindings(pattern);
742    let mut seen: HashMap<&str, &Span> = HashMap::default();
743    for (name, span) in &bindings {
744        if let Some(first_span) = seen.get(name.as_str()) {
745            sink.push(diagnostics::infer::duplicate_binding_in_pattern(
746                name,
747                *(*first_span),
748                *span,
749            ));
750        } else {
751            seen.insert(name, span);
752        }
753    }
754}
755
756pub(crate) fn check_binding_pattern(sink: &diagnostics::DiagnosticSink, pattern: &Pattern) {
757    if matches!(pattern, Pattern::Literal { .. }) {
758        sink.push(diagnostics::infer::literal_pattern_in_binding(
759            pattern.get_span(),
760        ));
761    }
762
763    if matches!(pattern, Pattern::Or { .. }) {
764        sink.push(diagnostics::infer::or_pattern_in_irrefutable_context(
765            pattern.get_span(),
766        ));
767    }
768}
769
770pub(crate) fn check_receiver(
771    sink: &diagnostics::DiagnosticSink,
772    method: &Expression,
773    impl_ty: &Type,
774) {
775    let Expression::Function { params, .. } = method else {
776        return;
777    };
778    let Some(first_param) = params.first() else {
779        return;
780    };
781    let Pattern::Identifier { identifier, span } = &first_param.pattern else {
782        return;
783    };
784
785    let receiver_ty = first_param.ty.strip_refs();
786    let types_match = receiver_ty == *impl_ty;
787
788    if types_match && identifier != "self" {
789        sink.push(diagnostics::infer::receiver_must_be_named_self(
790            identifier, *span,
791        ));
792    }
793
794    if !types_match && identifier == "self" {
795        let annotation_span = first_param
796            .annotation
797            .as_ref()
798            .map(|a| a.get_span())
799            .unwrap_or_else(|| *span);
800        let impl_type_name = impl_ty.get_name().unwrap_or_default();
801        let receiver_type_name = receiver_ty.get_name().unwrap_or_default();
802        sink.push(diagnostics::infer::receiver_type_mismatch(
803            impl_type_name,
804            receiver_type_name,
805            annotation_span,
806        ));
807    }
808}
809
810pub fn check_interface_visibility(
811    store: &crate::store::Store,
812    module_id: &str,
813    sink: &diagnostics::DiagnosticSink,
814) {
815    let module = match store.get_module(module_id) {
816        Some(m) => m,
817        None => return,
818    };
819
820    let non_pub_interfaces: HashMap<String, HashSet<String>> = module
821        .definitions
822        .iter()
823        .filter(|(key, _)| key.starts_with(&format!("{}.", module_id)))
824        .filter_map(|(_, definition)| {
825            if let syntax::program::Definition::Interface {
826                visibility: syntax::program::Visibility::Private,
827                definition: interface_data,
828                ..
829            } = definition
830            {
831                let method_names = interface_data
832                    .methods
833                    .keys()
834                    .map(|k| k.to_string())
835                    .collect();
836                Some((interface_data.name.to_string(), method_names))
837            } else {
838                None
839            }
840        })
841        .collect();
842
843    if non_pub_interfaces.is_empty() {
844        return;
845    }
846
847    for (_, definition) in module
848        .definitions
849        .iter()
850        .filter(|(key, _)| key.starts_with(&format!("{}.", module_id)))
851    {
852        if let syntax::program::Definition::Struct {
853            methods,
854            name,
855            name_span,
856            ..
857        } = definition
858        {
859            for method_name in methods.keys() {
860                for (interface_name, interface_methods) in &non_pub_interfaces {
861                    if interface_methods.contains(method_name.as_str()) {
862                        let method_key = format!("{}.{}.{}", module_id, name, method_name);
863                        let method_is_pub = module
864                            .definitions
865                            .get(method_key.as_str())
866                            .map(|definition| definition.visibility().is_public())
867                            .unwrap_or(false);
868
869                        if method_is_pub {
870                            sink.push(diagnostics::infer::non_pub_interface_with_pub_impl(
871                                interface_name,
872                                name,
873                                *name_span,
874                            ));
875                            return;
876                        }
877                    }
878                }
879            }
880        }
881    }
882}