Skip to main content

harn_parser/
typechecker.rs

1use std::collections::BTreeMap;
2
3use crate::ast::*;
4use harn_lexer::Span;
5
6/// A diagnostic produced by the type checker.
7#[derive(Debug, Clone)]
8pub struct TypeDiagnostic {
9    pub message: String,
10    pub severity: DiagnosticSeverity,
11    pub span: Option<Span>,
12    pub help: Option<String>,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum DiagnosticSeverity {
17    Error,
18    Warning,
19}
20
21/// Inferred type of an expression. None means unknown/untyped (gradual typing).
22type InferredType = Option<TypeExpr>;
23
24/// Scope for tracking variable types.
25#[derive(Debug, Clone)]
26struct TypeScope {
27    /// Variable name → inferred type.
28    vars: BTreeMap<String, InferredType>,
29    /// Function name → (param types, return type).
30    functions: BTreeMap<String, FnSignature>,
31    /// Named type aliases.
32    type_aliases: BTreeMap<String, TypeExpr>,
33    /// Enum declarations: name → variant names.
34    enums: BTreeMap<String, Vec<String>>,
35    /// Interface declarations: name → method signatures.
36    interfaces: BTreeMap<String, Vec<InterfaceMethod>>,
37    /// Struct declarations: name → field types.
38    structs: BTreeMap<String, Vec<(String, InferredType)>>,
39    /// Impl block methods: type_name → method signatures.
40    impl_methods: BTreeMap<String, Vec<ImplMethodSig>>,
41    /// Generic type parameter names in scope (treated as compatible with any type).
42    generic_type_params: std::collections::BTreeSet<String>,
43    /// Where-clause constraints: type_param → interface_bound.
44    /// Used for definition-site checking of generic function bodies.
45    where_constraints: BTreeMap<String, String>,
46    parent: Option<Box<TypeScope>>,
47}
48
49/// Method signature extracted from an impl block (for interface checking).
50#[derive(Debug, Clone)]
51struct ImplMethodSig {
52    name: String,
53    /// Number of parameters excluding `self`.
54    param_count: usize,
55    /// Parameter types (excluding `self`), None means untyped.
56    param_types: Vec<Option<TypeExpr>>,
57    /// Return type, None means untyped.
58    return_type: Option<TypeExpr>,
59}
60
61#[derive(Debug, Clone)]
62struct FnSignature {
63    params: Vec<(String, InferredType)>,
64    return_type: InferredType,
65    /// Generic type parameter names declared on the function.
66    type_param_names: Vec<String>,
67    /// Number of required parameters (those without defaults).
68    required_params: usize,
69    /// Where-clause constraints: (type_param_name, interface_bound).
70    where_clauses: Vec<(String, String)>,
71}
72
73impl TypeScope {
74    fn new() -> Self {
75        Self {
76            vars: BTreeMap::new(),
77            functions: BTreeMap::new(),
78            type_aliases: BTreeMap::new(),
79            enums: BTreeMap::new(),
80            interfaces: BTreeMap::new(),
81            structs: BTreeMap::new(),
82            impl_methods: BTreeMap::new(),
83            generic_type_params: std::collections::BTreeSet::new(),
84            where_constraints: BTreeMap::new(),
85            parent: None,
86        }
87    }
88
89    fn child(&self) -> Self {
90        Self {
91            vars: BTreeMap::new(),
92            functions: BTreeMap::new(),
93            type_aliases: BTreeMap::new(),
94            enums: BTreeMap::new(),
95            interfaces: BTreeMap::new(),
96            structs: BTreeMap::new(),
97            impl_methods: BTreeMap::new(),
98            generic_type_params: std::collections::BTreeSet::new(),
99            where_constraints: BTreeMap::new(),
100            parent: Some(Box::new(self.clone())),
101        }
102    }
103
104    fn get_var(&self, name: &str) -> Option<&InferredType> {
105        self.vars
106            .get(name)
107            .or_else(|| self.parent.as_ref()?.get_var(name))
108    }
109
110    fn get_fn(&self, name: &str) -> Option<&FnSignature> {
111        self.functions
112            .get(name)
113            .or_else(|| self.parent.as_ref()?.get_fn(name))
114    }
115
116    fn resolve_type(&self, name: &str) -> Option<&TypeExpr> {
117        self.type_aliases
118            .get(name)
119            .or_else(|| self.parent.as_ref()?.resolve_type(name))
120    }
121
122    fn is_generic_type_param(&self, name: &str) -> bool {
123        self.generic_type_params.contains(name)
124            || self
125                .parent
126                .as_ref()
127                .is_some_and(|p| p.is_generic_type_param(name))
128    }
129
130    fn get_where_constraint(&self, type_param: &str) -> Option<&str> {
131        self.where_constraints
132            .get(type_param)
133            .map(|s| s.as_str())
134            .or_else(|| {
135                self.parent
136                    .as_ref()
137                    .and_then(|p| p.get_where_constraint(type_param))
138            })
139    }
140
141    fn get_enum(&self, name: &str) -> Option<&Vec<String>> {
142        self.enums
143            .get(name)
144            .or_else(|| self.parent.as_ref()?.get_enum(name))
145    }
146
147    fn get_interface(&self, name: &str) -> Option<&Vec<InterfaceMethod>> {
148        self.interfaces
149            .get(name)
150            .or_else(|| self.parent.as_ref()?.get_interface(name))
151    }
152
153    fn get_struct(&self, name: &str) -> Option<&Vec<(String, InferredType)>> {
154        self.structs
155            .get(name)
156            .or_else(|| self.parent.as_ref()?.get_struct(name))
157    }
158
159    fn get_impl_methods(&self, name: &str) -> Option<&Vec<ImplMethodSig>> {
160        self.impl_methods
161            .get(name)
162            .or_else(|| self.parent.as_ref()?.get_impl_methods(name))
163    }
164
165    fn define_var(&mut self, name: &str, ty: InferredType) {
166        self.vars.insert(name.to_string(), ty);
167    }
168
169    fn define_fn(&mut self, name: &str, sig: FnSignature) {
170        self.functions.insert(name.to_string(), sig);
171    }
172}
173
174/// Known return types for builtin functions.
175fn builtin_return_type(name: &str) -> InferredType {
176    match name {
177        "log" | "print" | "println" | "write_file" | "sleep" | "cancel" | "exit"
178        | "delete_file" | "mkdir" | "copy_file" | "append_file" => {
179            Some(TypeExpr::Named("nil".into()))
180        }
181        "type_of"
182        | "to_string"
183        | "json_stringify"
184        | "read_file"
185        | "http_get"
186        | "http_post"
187        | "llm_call"
188        | "regex_replace"
189        | "path_join"
190        | "temp_dir"
191        | "date_format"
192        | "format"
193        | "compute_content_hash" => Some(TypeExpr::Named("string".into())),
194        "to_int" | "timer_end" | "elapsed" | "sign" => Some(TypeExpr::Named("int".into())),
195        "to_float" | "timestamp" | "date_parse" | "sin" | "cos" | "tan" | "asin" | "acos"
196        | "atan" | "atan2" | "log2" | "log10" | "ln" | "exp" | "pi" | "e" => {
197            Some(TypeExpr::Named("float".into()))
198        }
199        "file_exists" | "json_validate" | "is_nan" | "is_infinite" | "set_contains" => {
200            Some(TypeExpr::Named("bool".into()))
201        }
202        "list_dir" | "mcp_list_tools" | "mcp_list_resources" | "mcp_list_prompts" | "to_list"
203        | "regex_captures" => Some(TypeExpr::Named("list".into())),
204        "stat" | "exec" | "shell" | "date_now" | "agent_loop" | "llm_info" | "llm_usage"
205        | "timer_start" | "metadata_get" | "mcp_server_info" | "mcp_get_prompt" => {
206            Some(TypeExpr::Named("dict".into()))
207        }
208        "metadata_set"
209        | "metadata_save"
210        | "metadata_refresh_hashes"
211        | "invalidate_facts"
212        | "log_json"
213        | "mcp_disconnect" => Some(TypeExpr::Named("nil".into())),
214        "env" | "regex_match" => Some(TypeExpr::Union(vec![
215            TypeExpr::Named("string".into()),
216            TypeExpr::Named("nil".into()),
217        ])),
218        "json_parse" | "json_extract" => None, // could be any type
219        _ => None,
220    }
221}
222
223/// Check if a name is a known builtin.
224fn is_builtin(name: &str) -> bool {
225    matches!(
226        name,
227        "log"
228            | "print"
229            | "println"
230            | "type_of"
231            | "to_string"
232            | "to_int"
233            | "to_float"
234            | "json_stringify"
235            | "json_parse"
236            | "env"
237            | "timestamp"
238            | "sleep"
239            | "read_file"
240            | "write_file"
241            | "exit"
242            | "regex_match"
243            | "regex_replace"
244            | "regex_captures"
245            | "http_get"
246            | "http_post"
247            | "llm_call"
248            | "agent_loop"
249            | "await"
250            | "cancel"
251            | "file_exists"
252            | "delete_file"
253            | "list_dir"
254            | "mkdir"
255            | "path_join"
256            | "copy_file"
257            | "append_file"
258            | "temp_dir"
259            | "stat"
260            | "exec"
261            | "shell"
262            | "date_now"
263            | "date_format"
264            | "date_parse"
265            | "format"
266            | "json_validate"
267            | "json_extract"
268            | "trim"
269            | "lowercase"
270            | "uppercase"
271            | "split"
272            | "starts_with"
273            | "ends_with"
274            | "contains"
275            | "replace"
276            | "join"
277            | "len"
278            | "substring"
279            | "dirname"
280            | "basename"
281            | "extname"
282            | "sin"
283            | "cos"
284            | "tan"
285            | "asin"
286            | "acos"
287            | "atan"
288            | "atan2"
289            | "log2"
290            | "log10"
291            | "ln"
292            | "exp"
293            | "pi"
294            | "e"
295            | "sign"
296            | "is_nan"
297            | "is_infinite"
298            | "set"
299            | "set_add"
300            | "set_remove"
301            | "set_contains"
302            | "set_union"
303            | "set_intersect"
304            | "set_difference"
305            | "to_list"
306    )
307}
308
309/// The static type checker.
310pub struct TypeChecker {
311    diagnostics: Vec<TypeDiagnostic>,
312    scope: TypeScope,
313}
314
315impl TypeChecker {
316    pub fn new() -> Self {
317        Self {
318            diagnostics: Vec::new(),
319            scope: TypeScope::new(),
320        }
321    }
322
323    /// Check a program and return diagnostics.
324    pub fn check(mut self, program: &[SNode]) -> Vec<TypeDiagnostic> {
325        // First pass: register type and enum declarations into root scope
326        Self::register_declarations_into(&mut self.scope, program);
327
328        // Also scan pipeline bodies for declarations
329        for snode in program {
330            if let Node::Pipeline { body, .. } = &snode.node {
331                Self::register_declarations_into(&mut self.scope, body);
332            }
333        }
334
335        // Check each top-level node
336        for snode in program {
337            match &snode.node {
338                Node::Pipeline { params, body, .. } => {
339                    let mut child = self.scope.child();
340                    for p in params {
341                        child.define_var(p, None);
342                    }
343                    self.check_block(body, &mut child);
344                }
345                Node::FnDecl {
346                    name,
347                    type_params,
348                    params,
349                    return_type,
350                    where_clauses,
351                    body,
352                    ..
353                } => {
354                    let required_params =
355                        params.iter().filter(|p| p.default_value.is_none()).count();
356                    let sig = FnSignature {
357                        params: params
358                            .iter()
359                            .map(|p| (p.name.clone(), p.type_expr.clone()))
360                            .collect(),
361                        return_type: return_type.clone(),
362                        type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
363                        required_params,
364                        where_clauses: where_clauses
365                            .iter()
366                            .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
367                            .collect(),
368                    };
369                    self.scope.define_fn(name, sig);
370                    self.check_fn_body(type_params, params, return_type, body, where_clauses);
371                }
372                _ => {
373                    let mut scope = self.scope.clone();
374                    self.check_node(snode, &mut scope);
375                    // Merge any new definitions back into the top-level scope
376                    for (name, ty) in scope.vars {
377                        self.scope.vars.entry(name).or_insert(ty);
378                    }
379                }
380            }
381        }
382
383        self.diagnostics
384    }
385
386    /// Register type, enum, interface, and struct declarations from AST nodes into a scope.
387    fn register_declarations_into(scope: &mut TypeScope, nodes: &[SNode]) {
388        for snode in nodes {
389            match &snode.node {
390                Node::TypeDecl { name, type_expr } => {
391                    scope.type_aliases.insert(name.clone(), type_expr.clone());
392                }
393                Node::EnumDecl { name, variants } => {
394                    let variant_names: Vec<String> =
395                        variants.iter().map(|v| v.name.clone()).collect();
396                    scope.enums.insert(name.clone(), variant_names);
397                }
398                Node::InterfaceDecl { name, methods } => {
399                    scope.interfaces.insert(name.clone(), methods.clone());
400                }
401                Node::StructDecl { name, fields } => {
402                    let field_types: Vec<(String, InferredType)> = fields
403                        .iter()
404                        .map(|f| (f.name.clone(), f.type_expr.clone()))
405                        .collect();
406                    scope.structs.insert(name.clone(), field_types);
407                }
408                Node::ImplBlock {
409                    type_name, methods, ..
410                } => {
411                    let sigs: Vec<ImplMethodSig> = methods
412                        .iter()
413                        .filter_map(|m| {
414                            if let Node::FnDecl {
415                                name,
416                                params,
417                                return_type,
418                                ..
419                            } = &m.node
420                            {
421                                let non_self: Vec<_> =
422                                    params.iter().filter(|p| p.name != "self").collect();
423                                let param_count = non_self.len();
424                                let param_types: Vec<Option<TypeExpr>> =
425                                    non_self.iter().map(|p| p.type_expr.clone()).collect();
426                                Some(ImplMethodSig {
427                                    name: name.clone(),
428                                    param_count,
429                                    param_types,
430                                    return_type: return_type.clone(),
431                                })
432                            } else {
433                                None
434                            }
435                        })
436                        .collect();
437                    scope.impl_methods.insert(type_name.clone(), sigs);
438                }
439                _ => {}
440            }
441        }
442    }
443
444    fn check_block(&mut self, stmts: &[SNode], scope: &mut TypeScope) {
445        for stmt in stmts {
446            self.check_node(stmt, scope);
447        }
448    }
449
450    /// Define variables from a destructuring pattern in the given scope (as unknown type).
451    fn define_pattern_vars(pattern: &BindingPattern, scope: &mut TypeScope) {
452        match pattern {
453            BindingPattern::Identifier(name) => {
454                scope.define_var(name, None);
455            }
456            BindingPattern::Dict(fields) => {
457                for field in fields {
458                    let name = field.alias.as_deref().unwrap_or(&field.key);
459                    scope.define_var(name, None);
460                }
461            }
462            BindingPattern::List(elements) => {
463                for elem in elements {
464                    scope.define_var(&elem.name, None);
465                }
466            }
467        }
468    }
469
470    fn check_node(&mut self, snode: &SNode, scope: &mut TypeScope) {
471        let span = snode.span;
472        match &snode.node {
473            Node::LetBinding {
474                pattern,
475                type_ann,
476                value,
477            } => {
478                let inferred = self.infer_type(value, scope);
479                if let BindingPattern::Identifier(name) = pattern {
480                    if let Some(expected) = type_ann {
481                        if let Some(actual) = &inferred {
482                            if !self.types_compatible(expected, actual, scope) {
483                                let mut msg = format!(
484                                    "Type mismatch: '{}' declared as {}, but assigned {}",
485                                    name,
486                                    format_type(expected),
487                                    format_type(actual)
488                                );
489                                if let Some(detail) = shape_mismatch_detail(expected, actual) {
490                                    msg.push_str(&format!(" ({})", detail));
491                                }
492                                self.error_at(msg, span);
493                            }
494                        }
495                    }
496                    let ty = type_ann.clone().or(inferred);
497                    scope.define_var(name, ty);
498                } else {
499                    Self::define_pattern_vars(pattern, scope);
500                }
501            }
502
503            Node::VarBinding {
504                pattern,
505                type_ann,
506                value,
507            } => {
508                let inferred = self.infer_type(value, scope);
509                if let BindingPattern::Identifier(name) = pattern {
510                    if let Some(expected) = type_ann {
511                        if let Some(actual) = &inferred {
512                            if !self.types_compatible(expected, actual, scope) {
513                                let mut msg = format!(
514                                    "Type mismatch: '{}' declared as {}, but assigned {}",
515                                    name,
516                                    format_type(expected),
517                                    format_type(actual)
518                                );
519                                if let Some(detail) = shape_mismatch_detail(expected, actual) {
520                                    msg.push_str(&format!(" ({})", detail));
521                                }
522                                self.error_at(msg, span);
523                            }
524                        }
525                    }
526                    let ty = type_ann.clone().or(inferred);
527                    scope.define_var(name, ty);
528                } else {
529                    Self::define_pattern_vars(pattern, scope);
530                }
531            }
532
533            Node::FnDecl {
534                name,
535                type_params,
536                params,
537                return_type,
538                where_clauses,
539                body,
540                ..
541            } => {
542                let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
543                let sig = FnSignature {
544                    params: params
545                        .iter()
546                        .map(|p| (p.name.clone(), p.type_expr.clone()))
547                        .collect(),
548                    return_type: return_type.clone(),
549                    type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
550                    required_params,
551                    where_clauses: where_clauses
552                        .iter()
553                        .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
554                        .collect(),
555                };
556                scope.define_fn(name, sig.clone());
557                scope.define_var(name, None);
558                self.check_fn_body(type_params, params, return_type, body, where_clauses);
559            }
560
561            Node::FunctionCall { name, args } => {
562                self.check_call(name, args, scope, span);
563            }
564
565            Node::IfElse {
566                condition,
567                then_body,
568                else_body,
569            } => {
570                self.check_node(condition, scope);
571                let mut then_scope = scope.child();
572                // Narrow union types after nil checks: if x != nil, narrow x
573                if let Some((var_name, narrowed)) = Self::extract_nil_narrowing(condition, scope) {
574                    then_scope.define_var(&var_name, narrowed);
575                }
576                self.check_block(then_body, &mut then_scope);
577                if let Some(else_body) = else_body {
578                    let mut else_scope = scope.child();
579                    self.check_block(else_body, &mut else_scope);
580                }
581            }
582
583            Node::ForIn {
584                pattern,
585                iterable,
586                body,
587            } => {
588                self.check_node(iterable, scope);
589                let mut loop_scope = scope.child();
590                if let BindingPattern::Identifier(variable) = pattern {
591                    // Infer loop variable type from iterable
592                    let elem_type = match self.infer_type(iterable, scope) {
593                        Some(TypeExpr::List(inner)) => Some(*inner),
594                        Some(TypeExpr::Named(n)) if n == "string" => {
595                            Some(TypeExpr::Named("string".into()))
596                        }
597                        _ => None,
598                    };
599                    loop_scope.define_var(variable, elem_type);
600                } else {
601                    Self::define_pattern_vars(pattern, &mut loop_scope);
602                }
603                self.check_block(body, &mut loop_scope);
604            }
605
606            Node::WhileLoop { condition, body } => {
607                self.check_node(condition, scope);
608                let mut loop_scope = scope.child();
609                self.check_block(body, &mut loop_scope);
610            }
611
612            Node::TryCatch {
613                body,
614                error_var,
615                catch_body,
616                finally_body,
617                ..
618            } => {
619                let mut try_scope = scope.child();
620                self.check_block(body, &mut try_scope);
621                let mut catch_scope = scope.child();
622                if let Some(var) = error_var {
623                    catch_scope.define_var(var, None);
624                }
625                self.check_block(catch_body, &mut catch_scope);
626                if let Some(fb) = finally_body {
627                    let mut finally_scope = scope.child();
628                    self.check_block(fb, &mut finally_scope);
629                }
630            }
631
632            Node::TryExpr { body } => {
633                let mut try_scope = scope.child();
634                self.check_block(body, &mut try_scope);
635            }
636
637            Node::ReturnStmt {
638                value: Some(val), ..
639            } => {
640                self.check_node(val, scope);
641            }
642
643            Node::Assignment {
644                target, value, op, ..
645            } => {
646                self.check_node(value, scope);
647                if let Node::Identifier(name) = &target.node {
648                    if let Some(Some(var_type)) = scope.get_var(name) {
649                        let value_type = self.infer_type(value, scope);
650                        let assigned = if let Some(op) = op {
651                            let var_inferred = scope.get_var(name).cloned().flatten();
652                            infer_binary_op_type(op, &var_inferred, &value_type)
653                        } else {
654                            value_type
655                        };
656                        if let Some(actual) = &assigned {
657                            if !self.types_compatible(var_type, actual, scope) {
658                                self.error_at(
659                                    format!(
660                                        "Type mismatch: cannot assign {} to '{}' (declared as {})",
661                                        format_type(actual),
662                                        name,
663                                        format_type(var_type)
664                                    ),
665                                    span,
666                                );
667                            }
668                        }
669                    }
670                }
671            }
672
673            Node::TypeDecl { name, type_expr } => {
674                scope.type_aliases.insert(name.clone(), type_expr.clone());
675            }
676
677            Node::EnumDecl { name, variants } => {
678                let variant_names: Vec<String> = variants.iter().map(|v| v.name.clone()).collect();
679                scope.enums.insert(name.clone(), variant_names);
680            }
681
682            Node::StructDecl { name, fields } => {
683                let field_types: Vec<(String, InferredType)> = fields
684                    .iter()
685                    .map(|f| (f.name.clone(), f.type_expr.clone()))
686                    .collect();
687                scope.structs.insert(name.clone(), field_types);
688            }
689
690            Node::InterfaceDecl { name, methods } => {
691                scope.interfaces.insert(name.clone(), methods.clone());
692            }
693
694            Node::ImplBlock {
695                type_name, methods, ..
696            } => {
697                // Register impl methods for interface satisfaction checking
698                let sigs: Vec<ImplMethodSig> = methods
699                    .iter()
700                    .filter_map(|m| {
701                        if let Node::FnDecl {
702                            name,
703                            params,
704                            return_type,
705                            ..
706                        } = &m.node
707                        {
708                            let non_self: Vec<_> =
709                                params.iter().filter(|p| p.name != "self").collect();
710                            let param_count = non_self.len();
711                            let param_types: Vec<Option<TypeExpr>> =
712                                non_self.iter().map(|p| p.type_expr.clone()).collect();
713                            Some(ImplMethodSig {
714                                name: name.clone(),
715                                param_count,
716                                param_types,
717                                return_type: return_type.clone(),
718                            })
719                        } else {
720                            None
721                        }
722                    })
723                    .collect();
724                scope.impl_methods.insert(type_name.clone(), sigs);
725                for method_sn in methods {
726                    self.check_node(method_sn, scope);
727                }
728            }
729
730            Node::TryOperator { operand } => {
731                self.check_node(operand, scope);
732            }
733
734            Node::MatchExpr { value, arms } => {
735                self.check_node(value, scope);
736                let value_type = self.infer_type(value, scope);
737                for arm in arms {
738                    self.check_node(&arm.pattern, scope);
739                    // Check for incompatible literal pattern types
740                    if let Some(ref vt) = value_type {
741                        let value_type_name = format_type(vt);
742                        let mismatch = match &arm.pattern.node {
743                            Node::StringLiteral(_) => {
744                                !self.types_compatible(vt, &TypeExpr::Named("string".into()), scope)
745                            }
746                            Node::IntLiteral(_) => {
747                                !self.types_compatible(vt, &TypeExpr::Named("int".into()), scope)
748                                    && !self.types_compatible(
749                                        vt,
750                                        &TypeExpr::Named("float".into()),
751                                        scope,
752                                    )
753                            }
754                            Node::FloatLiteral(_) => {
755                                !self.types_compatible(vt, &TypeExpr::Named("float".into()), scope)
756                                    && !self.types_compatible(
757                                        vt,
758                                        &TypeExpr::Named("int".into()),
759                                        scope,
760                                    )
761                            }
762                            Node::BoolLiteral(_) => {
763                                !self.types_compatible(vt, &TypeExpr::Named("bool".into()), scope)
764                            }
765                            _ => false,
766                        };
767                        if mismatch {
768                            let pattern_type = match &arm.pattern.node {
769                                Node::StringLiteral(_) => "string",
770                                Node::IntLiteral(_) => "int",
771                                Node::FloatLiteral(_) => "float",
772                                Node::BoolLiteral(_) => "bool",
773                                _ => unreachable!(),
774                            };
775                            self.warning_at(
776                                format!(
777                                    "Match pattern type mismatch: matching {} against {} literal",
778                                    value_type_name, pattern_type
779                                ),
780                                arm.pattern.span,
781                            );
782                        }
783                    }
784                    let mut arm_scope = scope.child();
785                    self.check_block(&arm.body, &mut arm_scope);
786                }
787                self.check_match_exhaustiveness(value, arms, scope, span);
788            }
789
790            // Recurse into nested expressions + validate binary op types
791            Node::BinaryOp { op, left, right } => {
792                self.check_node(left, scope);
793                self.check_node(right, scope);
794                // Validate operator/type compatibility
795                let lt = self.infer_type(left, scope);
796                let rt = self.infer_type(right, scope);
797                if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (&lt, &rt) {
798                    match op.as_str() {
799                        "-" | "*" | "/" | "%" => {
800                            let numeric = ["int", "float"];
801                            if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
802                                self.warning_at(
803                                    format!(
804                                        "Operator '{op}' may not be valid for types {} and {}",
805                                        l, r
806                                    ),
807                                    span,
808                                );
809                            }
810                        }
811                        "+" => {
812                            // + is valid for int, float, string, list, dict
813                            let valid = ["int", "float", "string", "list", "dict"];
814                            if !valid.contains(&l.as_str()) && !valid.contains(&r.as_str()) {
815                                self.warning_at(
816                                    format!(
817                                        "Operator '+' may not be valid for types {} and {}",
818                                        l, r
819                                    ),
820                                    span,
821                                );
822                            }
823                        }
824                        _ => {}
825                    }
826                }
827            }
828            Node::UnaryOp { operand, .. } => {
829                self.check_node(operand, scope);
830            }
831            Node::MethodCall {
832                object,
833                method,
834                args,
835                ..
836            }
837            | Node::OptionalMethodCall {
838                object,
839                method,
840                args,
841                ..
842            } => {
843                self.check_node(object, scope);
844                for arg in args {
845                    self.check_node(arg, scope);
846                }
847                // Definition-site generic checking: if the object's type is a
848                // constrained generic param (where T: Interface), verify the
849                // method exists in the bound interface.
850                if let Some(TypeExpr::Named(type_name)) = self.infer_type(object, scope) {
851                    if scope.is_generic_type_param(&type_name) {
852                        if let Some(iface_name) = scope.get_where_constraint(&type_name) {
853                            if let Some(iface_methods) = scope.get_interface(iface_name) {
854                                let has_method = iface_methods.iter().any(|m| m.name == *method);
855                                if !has_method {
856                                    self.warning_at(
857                                        format!(
858                                            "Method '{}' not found in interface '{}' (constraint on '{}')",
859                                            method, iface_name, type_name
860                                        ),
861                                        span,
862                                    );
863                                }
864                            }
865                        }
866                    }
867                }
868            }
869            Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
870                self.check_node(object, scope);
871            }
872            Node::SubscriptAccess { object, index } => {
873                self.check_node(object, scope);
874                self.check_node(index, scope);
875            }
876            Node::SliceAccess { object, start, end } => {
877                self.check_node(object, scope);
878                if let Some(s) = start {
879                    self.check_node(s, scope);
880                }
881                if let Some(e) = end {
882                    self.check_node(e, scope);
883                }
884            }
885
886            // Terminals — nothing to check
887            _ => {}
888        }
889    }
890
891    fn check_fn_body(
892        &mut self,
893        type_params: &[TypeParam],
894        params: &[TypedParam],
895        return_type: &Option<TypeExpr>,
896        body: &[SNode],
897        where_clauses: &[WhereClause],
898    ) {
899        let mut fn_scope = self.scope.child();
900        // Register generic type parameters so they are treated as compatible
901        // with any concrete type during type checking.
902        for tp in type_params {
903            fn_scope.generic_type_params.insert(tp.name.clone());
904        }
905        // Store where-clause constraints for definition-site checking
906        for wc in where_clauses {
907            fn_scope
908                .where_constraints
909                .insert(wc.type_name.clone(), wc.bound.clone());
910        }
911        for param in params {
912            fn_scope.define_var(&param.name, param.type_expr.clone());
913            if let Some(default) = &param.default_value {
914                self.check_node(default, &mut fn_scope);
915            }
916        }
917        self.check_block(body, &mut fn_scope);
918
919        // Check return statements against declared return type
920        if let Some(ret_type) = return_type {
921            for stmt in body {
922                self.check_return_type(stmt, ret_type, &fn_scope);
923            }
924        }
925    }
926
927    fn check_return_type(&mut self, snode: &SNode, expected: &TypeExpr, scope: &TypeScope) {
928        let span = snode.span;
929        match &snode.node {
930            Node::ReturnStmt { value: Some(val) } => {
931                let inferred = self.infer_type(val, scope);
932                if let Some(actual) = &inferred {
933                    if !self.types_compatible(expected, actual, scope) {
934                        self.error_at(
935                            format!(
936                                "Return type mismatch: expected {}, got {}",
937                                format_type(expected),
938                                format_type(actual)
939                            ),
940                            span,
941                        );
942                    }
943                }
944            }
945            Node::IfElse {
946                then_body,
947                else_body,
948                ..
949            } => {
950                for stmt in then_body {
951                    self.check_return_type(stmt, expected, scope);
952                }
953                if let Some(else_body) = else_body {
954                    for stmt in else_body {
955                        self.check_return_type(stmt, expected, scope);
956                    }
957                }
958            }
959            _ => {}
960        }
961    }
962
963    /// Check if a match expression on an enum's `.variant` property covers all variants.
964    /// Extract narrowing info from nil-check conditions like `x != nil`.
965    /// Returns (var_name, narrowed_type) where narrowed_type removes nil from a union.
966    /// Check if a type satisfies an interface (Go-style implicit satisfaction).
967    /// A type satisfies an interface if its impl block has all the required methods.
968    fn satisfies_interface(
969        &self,
970        type_name: &str,
971        interface_name: &str,
972        scope: &TypeScope,
973    ) -> bool {
974        self.interface_mismatch_reason(type_name, interface_name, scope)
975            .is_none()
976    }
977
978    /// Return a detailed reason why a type does not satisfy an interface, or None
979    /// if it does satisfy it.  Used for producing actionable warning messages.
980    fn interface_mismatch_reason(
981        &self,
982        type_name: &str,
983        interface_name: &str,
984        scope: &TypeScope,
985    ) -> Option<String> {
986        let interface_methods = match scope.get_interface(interface_name) {
987            Some(methods) => methods,
988            None => return Some(format!("interface '{}' not found", interface_name)),
989        };
990        let impl_methods = match scope.get_impl_methods(type_name) {
991            Some(methods) => methods,
992            None => {
993                if interface_methods.is_empty() {
994                    return None;
995                }
996                let names: Vec<_> = interface_methods.iter().map(|m| m.name.as_str()).collect();
997                return Some(format!("missing method(s): {}", names.join(", ")));
998            }
999        };
1000        for iface_method in interface_methods {
1001            let iface_params: Vec<_> = iface_method
1002                .params
1003                .iter()
1004                .filter(|p| p.name != "self")
1005                .collect();
1006            let iface_param_count = iface_params.len();
1007            let matching_impl = impl_methods.iter().find(|im| im.name == iface_method.name);
1008            let impl_method = match matching_impl {
1009                Some(m) => m,
1010                None => {
1011                    return Some(format!("missing method '{}'", iface_method.name));
1012                }
1013            };
1014            if impl_method.param_count != iface_param_count {
1015                return Some(format!(
1016                    "method '{}' has {} parameter(s), expected {}",
1017                    iface_method.name, impl_method.param_count, iface_param_count
1018                ));
1019            }
1020            // Check parameter types where both sides specify them
1021            for (i, iface_param) in iface_params.iter().enumerate() {
1022                if let (Some(expected), Some(actual)) = (
1023                    &iface_param.type_expr,
1024                    impl_method.param_types.get(i).and_then(|t| t.as_ref()),
1025                ) {
1026                    if !self.types_compatible(expected, actual, scope) {
1027                        return Some(format!(
1028                            "method '{}' parameter {} has type '{}', expected '{}'",
1029                            iface_method.name,
1030                            i + 1,
1031                            format_type(actual),
1032                            format_type(expected),
1033                        ));
1034                    }
1035                }
1036            }
1037            // Check return type where both sides specify it
1038            if let (Some(expected_ret), Some(actual_ret)) =
1039                (&iface_method.return_type, &impl_method.return_type)
1040            {
1041                if !self.types_compatible(expected_ret, actual_ret, scope) {
1042                    return Some(format!(
1043                        "method '{}' returns '{}', expected '{}'",
1044                        iface_method.name,
1045                        format_type(actual_ret),
1046                        format_type(expected_ret),
1047                    ));
1048                }
1049            }
1050        }
1051        None
1052    }
1053
1054    /// Recursively extract type parameter bindings from matching param/arg types.
1055    /// E.g., param_type=list<T> + arg_type=list<Dog> → binds T=Dog.
1056    fn extract_type_bindings(
1057        param_type: &TypeExpr,
1058        arg_type: &TypeExpr,
1059        type_params: &std::collections::BTreeSet<String>,
1060        bindings: &mut BTreeMap<String, String>,
1061    ) {
1062        match (param_type, arg_type) {
1063            // Direct type param match: T → concrete
1064            (TypeExpr::Named(param_name), TypeExpr::Named(concrete))
1065                if type_params.contains(param_name) =>
1066            {
1067                bindings
1068                    .entry(param_name.clone())
1069                    .or_insert(concrete.clone());
1070            }
1071            // list<T> + list<Dog>
1072            (TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
1073                Self::extract_type_bindings(p_inner, a_inner, type_params, bindings);
1074            }
1075            // dict<K, V> + dict<string, int>
1076            (TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
1077                Self::extract_type_bindings(pk, ak, type_params, bindings);
1078                Self::extract_type_bindings(pv, av, type_params, bindings);
1079            }
1080            _ => {}
1081        }
1082    }
1083
1084    fn extract_nil_narrowing(
1085        condition: &SNode,
1086        scope: &TypeScope,
1087    ) -> Option<(String, InferredType)> {
1088        if let Node::BinaryOp { op, left, right } = &condition.node {
1089            if op == "!=" {
1090                // Check for `x != nil` or `nil != x`
1091                let (var_node, nil_node) = if matches!(right.node, Node::NilLiteral) {
1092                    (left, right)
1093                } else if matches!(left.node, Node::NilLiteral) {
1094                    (right, left)
1095                } else {
1096                    return None;
1097                };
1098                let _ = nil_node;
1099                if let Node::Identifier(name) = &var_node.node {
1100                    // Look up the variable's type and narrow it
1101                    if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1102                        let narrowed: Vec<TypeExpr> = members
1103                            .iter()
1104                            .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
1105                            .cloned()
1106                            .collect();
1107                        return if narrowed.len() == 1 {
1108                            Some((name.clone(), Some(narrowed.into_iter().next().unwrap())))
1109                        } else if narrowed.is_empty() {
1110                            None
1111                        } else {
1112                            Some((name.clone(), Some(TypeExpr::Union(narrowed))))
1113                        };
1114                    }
1115                }
1116            }
1117        }
1118        None
1119    }
1120
1121    fn check_match_exhaustiveness(
1122        &mut self,
1123        value: &SNode,
1124        arms: &[MatchArm],
1125        scope: &TypeScope,
1126        span: Span,
1127    ) {
1128        // Detect pattern: match <expr>.variant { "VariantA" -> ... }
1129        let enum_name = match &value.node {
1130            Node::PropertyAccess { object, property } if property == "variant" => {
1131                // Infer the type of the object
1132                match self.infer_type(object, scope) {
1133                    Some(TypeExpr::Named(name)) => {
1134                        if scope.get_enum(&name).is_some() {
1135                            Some(name)
1136                        } else {
1137                            None
1138                        }
1139                    }
1140                    _ => None,
1141                }
1142            }
1143            _ => {
1144                // Direct match on an enum value: match <expr> { ... }
1145                match self.infer_type(value, scope) {
1146                    Some(TypeExpr::Named(name)) if scope.get_enum(&name).is_some() => Some(name),
1147                    _ => None,
1148                }
1149            }
1150        };
1151
1152        let Some(enum_name) = enum_name else {
1153            return;
1154        };
1155        let Some(variants) = scope.get_enum(&enum_name) else {
1156            return;
1157        };
1158
1159        // Collect variant names covered by match arms
1160        let mut covered: Vec<String> = Vec::new();
1161        let mut has_wildcard = false;
1162
1163        for arm in arms {
1164            match &arm.pattern.node {
1165                // String literal pattern (matching on .variant): "VariantA"
1166                Node::StringLiteral(s) => covered.push(s.clone()),
1167                // Identifier pattern acts as a wildcard/catch-all
1168                Node::Identifier(name) if name == "_" || !variants.contains(name) => {
1169                    has_wildcard = true;
1170                }
1171                // Direct enum construct pattern: EnumName.Variant
1172                Node::EnumConstruct { variant, .. } => covered.push(variant.clone()),
1173                // PropertyAccess pattern: EnumName.Variant (no args)
1174                Node::PropertyAccess { property, .. } => covered.push(property.clone()),
1175                _ => {
1176                    // Unknown pattern shape — conservatively treat as wildcard
1177                    has_wildcard = true;
1178                }
1179            }
1180        }
1181
1182        if has_wildcard {
1183            return;
1184        }
1185
1186        let missing: Vec<&String> = variants.iter().filter(|v| !covered.contains(v)).collect();
1187        if !missing.is_empty() {
1188            let missing_str = missing
1189                .iter()
1190                .map(|s| format!("\"{}\"", s))
1191                .collect::<Vec<_>>()
1192                .join(", ");
1193            self.warning_at(
1194                format!(
1195                    "Non-exhaustive match on enum {}: missing variants {}",
1196                    enum_name, missing_str
1197                ),
1198                span,
1199            );
1200        }
1201    }
1202
1203    fn check_call(&mut self, name: &str, args: &[SNode], scope: &mut TypeScope, span: Span) {
1204        // Check against known function signatures
1205        let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
1206        if let Some(sig) = scope.get_fn(name).cloned() {
1207            if !has_spread
1208                && !is_builtin(name)
1209                && (args.len() < sig.required_params || args.len() > sig.params.len())
1210            {
1211                let expected = if sig.required_params == sig.params.len() {
1212                    format!("{}", sig.params.len())
1213                } else {
1214                    format!("{}-{}", sig.required_params, sig.params.len())
1215                };
1216                self.warning_at(
1217                    format!(
1218                        "Function '{}' expects {} arguments, got {}",
1219                        name,
1220                        expected,
1221                        args.len()
1222                    ),
1223                    span,
1224                );
1225            }
1226            // Build a scope that includes the function's generic type params
1227            // so they are treated as compatible with any concrete type.
1228            let call_scope = if sig.type_param_names.is_empty() {
1229                scope.clone()
1230            } else {
1231                let mut s = scope.child();
1232                for tp_name in &sig.type_param_names {
1233                    s.generic_type_params.insert(tp_name.clone());
1234                }
1235                s
1236            };
1237            for (i, (arg, (param_name, param_type))) in
1238                args.iter().zip(sig.params.iter()).enumerate()
1239            {
1240                if let Some(expected) = param_type {
1241                    let actual = self.infer_type(arg, scope);
1242                    if let Some(actual) = &actual {
1243                        if !self.types_compatible(expected, actual, &call_scope) {
1244                            self.error_at(
1245                                format!(
1246                                    "Argument {} ('{}'): expected {}, got {}",
1247                                    i + 1,
1248                                    param_name,
1249                                    format_type(expected),
1250                                    format_type(actual)
1251                                ),
1252                                arg.span,
1253                            );
1254                        }
1255                    }
1256                }
1257            }
1258            // Enforce where-clause constraints at call site
1259            if !sig.where_clauses.is_empty() {
1260                // Build mapping: type_param → concrete type from inferred args.
1261                // Recursively walks Generic types so list<T> + list<Dog> binds T=Dog.
1262                let mut type_bindings: BTreeMap<String, String> = BTreeMap::new();
1263                let type_param_set: std::collections::BTreeSet<String> =
1264                    sig.type_param_names.iter().cloned().collect();
1265                for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
1266                    if let Some(param_ty) = param_type {
1267                        if let Some(arg_ty) = self.infer_type(arg, scope) {
1268                            Self::extract_type_bindings(
1269                                param_ty,
1270                                &arg_ty,
1271                                &type_param_set,
1272                                &mut type_bindings,
1273                            );
1274                        }
1275                    }
1276                }
1277                for (type_param, bound) in &sig.where_clauses {
1278                    if let Some(concrete_type) = type_bindings.get(type_param) {
1279                        if let Some(reason) =
1280                            self.interface_mismatch_reason(concrete_type, bound, scope)
1281                        {
1282                            self.warning_at(
1283                                format!(
1284                                    "Type '{}' does not satisfy interface '{}': {} \
1285                                     (required by constraint `where {}: {}`)",
1286                                    concrete_type, bound, reason, type_param, bound
1287                                ),
1288                                span,
1289                            );
1290                        }
1291                    }
1292                }
1293            }
1294        }
1295        // Check args recursively
1296        for arg in args {
1297            self.check_node(arg, scope);
1298        }
1299    }
1300
1301    /// Infer the type of an expression.
1302    fn infer_type(&self, snode: &SNode, scope: &TypeScope) -> InferredType {
1303        match &snode.node {
1304            Node::IntLiteral(_) => Some(TypeExpr::Named("int".into())),
1305            Node::FloatLiteral(_) => Some(TypeExpr::Named("float".into())),
1306            Node::StringLiteral(_) | Node::InterpolatedString(_) => {
1307                Some(TypeExpr::Named("string".into()))
1308            }
1309            Node::BoolLiteral(_) => Some(TypeExpr::Named("bool".into())),
1310            Node::NilLiteral => Some(TypeExpr::Named("nil".into())),
1311            Node::ListLiteral(_) => Some(TypeExpr::Named("list".into())),
1312            Node::DictLiteral(entries) => {
1313                // Infer shape type when all keys are string literals
1314                let mut fields = Vec::new();
1315                let mut all_string_keys = true;
1316                for entry in entries {
1317                    if let Node::StringLiteral(key) = &entry.key.node {
1318                        let val_type = self
1319                            .infer_type(&entry.value, scope)
1320                            .unwrap_or(TypeExpr::Named("nil".into()));
1321                        fields.push(ShapeField {
1322                            name: key.clone(),
1323                            type_expr: val_type,
1324                            optional: false,
1325                        });
1326                    } else {
1327                        all_string_keys = false;
1328                        break;
1329                    }
1330                }
1331                if all_string_keys && !fields.is_empty() {
1332                    Some(TypeExpr::Shape(fields))
1333                } else {
1334                    Some(TypeExpr::Named("dict".into()))
1335                }
1336            }
1337            Node::Closure { params, body, .. } => {
1338                // If all params are typed and we can infer a return type, produce FnType
1339                let all_typed = params.iter().all(|p| p.type_expr.is_some());
1340                if all_typed && !params.is_empty() {
1341                    let param_types: Vec<TypeExpr> =
1342                        params.iter().filter_map(|p| p.type_expr.clone()).collect();
1343                    // Try to infer return type from last expression in body
1344                    let ret = body.last().and_then(|last| self.infer_type(last, scope));
1345                    if let Some(ret_type) = ret {
1346                        return Some(TypeExpr::FnType {
1347                            params: param_types,
1348                            return_type: Box::new(ret_type),
1349                        });
1350                    }
1351                }
1352                Some(TypeExpr::Named("closure".into()))
1353            }
1354
1355            Node::Identifier(name) => scope.get_var(name).cloned().flatten(),
1356
1357            Node::FunctionCall { name, .. } => {
1358                // Struct constructor calls return the struct type
1359                if scope.get_struct(name).is_some() {
1360                    return Some(TypeExpr::Named(name.clone()));
1361                }
1362                // Check user-defined function return types
1363                if let Some(sig) = scope.get_fn(name) {
1364                    return sig.return_type.clone();
1365                }
1366                // Check builtin return types
1367                builtin_return_type(name)
1368            }
1369
1370            Node::BinaryOp { op, left, right } => {
1371                let lt = self.infer_type(left, scope);
1372                let rt = self.infer_type(right, scope);
1373                infer_binary_op_type(op, &lt, &rt)
1374            }
1375
1376            Node::UnaryOp { op, operand } => {
1377                let t = self.infer_type(operand, scope);
1378                match op.as_str() {
1379                    "!" => Some(TypeExpr::Named("bool".into())),
1380                    "-" => t, // negation preserves type
1381                    _ => None,
1382                }
1383            }
1384
1385            Node::Ternary {
1386                true_expr,
1387                false_expr,
1388                ..
1389            } => {
1390                let tt = self.infer_type(true_expr, scope);
1391                let ft = self.infer_type(false_expr, scope);
1392                match (&tt, &ft) {
1393                    (Some(a), Some(b)) if a == b => tt,
1394                    (Some(a), Some(b)) => Some(TypeExpr::Union(vec![a.clone(), b.clone()])),
1395                    (Some(_), None) => tt,
1396                    (None, Some(_)) => ft,
1397                    (None, None) => None,
1398                }
1399            }
1400
1401            Node::EnumConstruct { enum_name, .. } => Some(TypeExpr::Named(enum_name.clone())),
1402
1403            Node::PropertyAccess { object, property } => {
1404                // EnumName.Variant → infer as the enum type
1405                if let Node::Identifier(name) = &object.node {
1406                    if scope.get_enum(name).is_some() {
1407                        return Some(TypeExpr::Named(name.clone()));
1408                    }
1409                }
1410                // .variant on an enum value → string
1411                if property == "variant" {
1412                    let obj_type = self.infer_type(object, scope);
1413                    if let Some(TypeExpr::Named(name)) = &obj_type {
1414                        if scope.get_enum(name).is_some() {
1415                            return Some(TypeExpr::Named("string".into()));
1416                        }
1417                    }
1418                }
1419                // Shape field access: obj.field → field type
1420                let obj_type = self.infer_type(object, scope);
1421                if let Some(TypeExpr::Shape(fields)) = &obj_type {
1422                    if let Some(field) = fields.iter().find(|f| f.name == *property) {
1423                        return Some(field.type_expr.clone());
1424                    }
1425                }
1426                None
1427            }
1428
1429            Node::SubscriptAccess { object, index } => {
1430                let obj_type = self.infer_type(object, scope);
1431                match &obj_type {
1432                    Some(TypeExpr::List(inner)) => Some(*inner.clone()),
1433                    Some(TypeExpr::DictType(_, v)) => Some(*v.clone()),
1434                    Some(TypeExpr::Shape(fields)) => {
1435                        // If index is a string literal, look up the field type
1436                        if let Node::StringLiteral(key) = &index.node {
1437                            fields
1438                                .iter()
1439                                .find(|f| &f.name == key)
1440                                .map(|f| f.type_expr.clone())
1441                        } else {
1442                            None
1443                        }
1444                    }
1445                    Some(TypeExpr::Named(n)) if n == "list" => None,
1446                    Some(TypeExpr::Named(n)) if n == "dict" => None,
1447                    Some(TypeExpr::Named(n)) if n == "string" => {
1448                        Some(TypeExpr::Named("string".into()))
1449                    }
1450                    _ => None,
1451                }
1452            }
1453            Node::SliceAccess { object, .. } => {
1454                // Slicing a list returns the same list type; slicing a string returns string
1455                let obj_type = self.infer_type(object, scope);
1456                match &obj_type {
1457                    Some(TypeExpr::List(_)) => obj_type,
1458                    Some(TypeExpr::Named(n)) if n == "list" => obj_type,
1459                    Some(TypeExpr::Named(n)) if n == "string" => {
1460                        Some(TypeExpr::Named("string".into()))
1461                    }
1462                    _ => None,
1463                }
1464            }
1465            Node::MethodCall { object, method, .. }
1466            | Node::OptionalMethodCall { object, method, .. } => {
1467                let obj_type = self.infer_type(object, scope);
1468                let is_dict = matches!(&obj_type, Some(TypeExpr::Named(n)) if n == "dict")
1469                    || matches!(&obj_type, Some(TypeExpr::DictType(..)));
1470                match method.as_str() {
1471                    // Shared: bool-returning methods
1472                    "contains" | "starts_with" | "ends_with" | "empty" | "has" | "any" | "all" => {
1473                        Some(TypeExpr::Named("bool".into()))
1474                    }
1475                    // Shared: int-returning methods
1476                    "count" | "index_of" => Some(TypeExpr::Named("int".into())),
1477                    // String methods
1478                    "trim" | "lowercase" | "uppercase" | "reverse" | "replace" | "substring"
1479                    | "pad_left" | "pad_right" | "repeat" | "join" => {
1480                        Some(TypeExpr::Named("string".into()))
1481                    }
1482                    "split" | "chars" => Some(TypeExpr::Named("list".into())),
1483                    // filter returns dict for dicts, list for lists
1484                    "filter" => {
1485                        if is_dict {
1486                            Some(TypeExpr::Named("dict".into()))
1487                        } else {
1488                            Some(TypeExpr::Named("list".into()))
1489                        }
1490                    }
1491                    // List methods
1492                    "map" | "flat_map" | "sort" => Some(TypeExpr::Named("list".into())),
1493                    "reduce" | "find" | "first" | "last" => None,
1494                    // Dict methods
1495                    "keys" | "values" | "entries" => Some(TypeExpr::Named("list".into())),
1496                    "merge" | "map_values" => Some(TypeExpr::Named("dict".into())),
1497                    // Conversions
1498                    "to_string" => Some(TypeExpr::Named("string".into())),
1499                    "to_int" => Some(TypeExpr::Named("int".into())),
1500                    "to_float" => Some(TypeExpr::Named("float".into())),
1501                    _ => None,
1502                }
1503            }
1504
1505            // TryOperator on Result<T, E> produces T
1506            Node::TryOperator { operand } => {
1507                match self.infer_type(operand, scope) {
1508                    Some(TypeExpr::Named(name)) if name == "Result" => None, // unknown inner type
1509                    _ => None,
1510                }
1511            }
1512
1513            _ => None,
1514        }
1515    }
1516
1517    /// Check if two types are compatible (actual can be assigned to expected).
1518    fn types_compatible(&self, expected: &TypeExpr, actual: &TypeExpr, scope: &TypeScope) -> bool {
1519        // Generic type parameters match anything.
1520        if let TypeExpr::Named(name) = expected {
1521            if scope.is_generic_type_param(name) {
1522                return true;
1523            }
1524        }
1525        if let TypeExpr::Named(name) = actual {
1526            if scope.is_generic_type_param(name) {
1527                return true;
1528            }
1529        }
1530        let expected = self.resolve_alias(expected, scope);
1531        let actual = self.resolve_alias(actual, scope);
1532
1533        // Interface satisfaction: if expected is an interface name, check if actual type
1534        // has all required methods (Go-style implicit satisfaction).
1535        if let TypeExpr::Named(iface_name) = &expected {
1536            if scope.get_interface(iface_name).is_some() {
1537                if let TypeExpr::Named(type_name) = &actual {
1538                    return self.satisfies_interface(type_name, iface_name, scope);
1539                }
1540                return false;
1541            }
1542        }
1543
1544        match (&expected, &actual) {
1545            (TypeExpr::Named(a), TypeExpr::Named(b)) => a == b || (a == "float" && b == "int"),
1546            (TypeExpr::Union(members), actual_type) => members
1547                .iter()
1548                .any(|m| self.types_compatible(m, actual_type, scope)),
1549            (expected_type, TypeExpr::Union(members)) => members
1550                .iter()
1551                .all(|m| self.types_compatible(expected_type, m, scope)),
1552            (TypeExpr::Shape(_), TypeExpr::Named(n)) if n == "dict" => true,
1553            (TypeExpr::Named(n), TypeExpr::Shape(_)) if n == "dict" => true,
1554            (TypeExpr::Shape(ef), TypeExpr::Shape(af)) => ef.iter().all(|expected_field| {
1555                if expected_field.optional {
1556                    return true;
1557                }
1558                af.iter().any(|actual_field| {
1559                    actual_field.name == expected_field.name
1560                        && self.types_compatible(
1561                            &expected_field.type_expr,
1562                            &actual_field.type_expr,
1563                            scope,
1564                        )
1565                })
1566            }),
1567            // dict<K, V> expected, Shape actual → all field values must match V
1568            (TypeExpr::DictType(ek, ev), TypeExpr::Shape(af)) => {
1569                let keys_ok = matches!(ek.as_ref(), TypeExpr::Named(n) if n == "string");
1570                keys_ok
1571                    && af
1572                        .iter()
1573                        .all(|f| self.types_compatible(ev, &f.type_expr, scope))
1574            }
1575            // Shape expected, dict<K, V> actual → gradual: allow since dict may have the fields
1576            (TypeExpr::Shape(_), TypeExpr::DictType(_, _)) => true,
1577            (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
1578                self.types_compatible(expected_inner, actual_inner, scope)
1579            }
1580            (TypeExpr::Named(n), TypeExpr::List(_)) if n == "list" => true,
1581            (TypeExpr::List(_), TypeExpr::Named(n)) if n == "list" => true,
1582            (TypeExpr::DictType(ek, ev), TypeExpr::DictType(ak, av)) => {
1583                self.types_compatible(ek, ak, scope) && self.types_compatible(ev, av, scope)
1584            }
1585            (TypeExpr::Named(n), TypeExpr::DictType(_, _)) if n == "dict" => true,
1586            (TypeExpr::DictType(_, _), TypeExpr::Named(n)) if n == "dict" => true,
1587            // FnType compatibility: params match positionally and return types match
1588            (
1589                TypeExpr::FnType {
1590                    params: ep,
1591                    return_type: er,
1592                },
1593                TypeExpr::FnType {
1594                    params: ap,
1595                    return_type: ar,
1596                },
1597            ) => {
1598                ep.len() == ap.len()
1599                    && ep
1600                        .iter()
1601                        .zip(ap.iter())
1602                        .all(|(e, a)| self.types_compatible(e, a, scope))
1603                    && self.types_compatible(er, ar, scope)
1604            }
1605            // FnType is compatible with Named("closure") for backward compat
1606            (TypeExpr::FnType { .. }, TypeExpr::Named(n)) if n == "closure" => true,
1607            (TypeExpr::Named(n), TypeExpr::FnType { .. }) if n == "closure" => true,
1608            _ => false,
1609        }
1610    }
1611
1612    fn resolve_alias<'a>(&self, ty: &'a TypeExpr, scope: &'a TypeScope) -> TypeExpr {
1613        if let TypeExpr::Named(name) = ty {
1614            if let Some(resolved) = scope.resolve_type(name) {
1615                return resolved.clone();
1616            }
1617        }
1618        ty.clone()
1619    }
1620
1621    fn error_at(&mut self, message: String, span: Span) {
1622        self.diagnostics.push(TypeDiagnostic {
1623            message,
1624            severity: DiagnosticSeverity::Error,
1625            span: Some(span),
1626            help: None,
1627        });
1628    }
1629
1630    #[allow(dead_code)]
1631    fn error_at_with_help(&mut self, message: String, span: Span, help: String) {
1632        self.diagnostics.push(TypeDiagnostic {
1633            message,
1634            severity: DiagnosticSeverity::Error,
1635            span: Some(span),
1636            help: Some(help),
1637        });
1638    }
1639
1640    fn warning_at(&mut self, message: String, span: Span) {
1641        self.diagnostics.push(TypeDiagnostic {
1642            message,
1643            severity: DiagnosticSeverity::Warning,
1644            span: Some(span),
1645            help: None,
1646        });
1647    }
1648
1649    #[allow(dead_code)]
1650    fn warning_at_with_help(&mut self, message: String, span: Span, help: String) {
1651        self.diagnostics.push(TypeDiagnostic {
1652            message,
1653            severity: DiagnosticSeverity::Warning,
1654            span: Some(span),
1655            help: Some(help),
1656        });
1657    }
1658}
1659
1660impl Default for TypeChecker {
1661    fn default() -> Self {
1662        Self::new()
1663    }
1664}
1665
1666/// Infer the result type of a binary operation.
1667fn infer_binary_op_type(op: &str, left: &InferredType, right: &InferredType) -> InferredType {
1668    match op {
1669        "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
1670            Some(TypeExpr::Named("bool".into()))
1671        }
1672        "+" => match (left, right) {
1673            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
1674                match (l.as_str(), r.as_str()) {
1675                    ("int", "int") => Some(TypeExpr::Named("int".into())),
1676                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
1677                    ("string", _) => Some(TypeExpr::Named("string".into())),
1678                    ("list", "list") => Some(TypeExpr::Named("list".into())),
1679                    ("dict", "dict") => Some(TypeExpr::Named("dict".into())),
1680                    _ => Some(TypeExpr::Named("string".into())),
1681                }
1682            }
1683            _ => None,
1684        },
1685        "-" | "*" | "/" | "%" => match (left, right) {
1686            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
1687                match (l.as_str(), r.as_str()) {
1688                    ("int", "int") => Some(TypeExpr::Named("int".into())),
1689                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
1690                    _ => None,
1691                }
1692            }
1693            _ => None,
1694        },
1695        "??" => match (left, right) {
1696            (Some(TypeExpr::Union(members)), _) => {
1697                let non_nil: Vec<_> = members
1698                    .iter()
1699                    .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
1700                    .cloned()
1701                    .collect();
1702                if non_nil.len() == 1 {
1703                    Some(non_nil[0].clone())
1704                } else if non_nil.is_empty() {
1705                    right.clone()
1706                } else {
1707                    Some(TypeExpr::Union(non_nil))
1708                }
1709            }
1710            _ => right.clone(),
1711        },
1712        "|>" => None,
1713        _ => None,
1714    }
1715}
1716
1717/// Format a type expression for display in error messages.
1718/// Produce a detail string describing why a Shape type is incompatible with
1719/// another Shape type — e.g. "missing field 'age' (int)" or "field 'name'
1720/// has type int, expected string".  Returns `None` if both types are not shapes.
1721pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
1722    if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
1723        let mut details = Vec::new();
1724        for field in ef {
1725            if field.optional {
1726                continue;
1727            }
1728            match af.iter().find(|f| f.name == field.name) {
1729                None => details.push(format!(
1730                    "missing field '{}' ({})",
1731                    field.name,
1732                    format_type(&field.type_expr)
1733                )),
1734                Some(actual_field) => {
1735                    let e_str = format_type(&field.type_expr);
1736                    let a_str = format_type(&actual_field.type_expr);
1737                    if e_str != a_str {
1738                        details.push(format!(
1739                            "field '{}' has type {}, expected {}",
1740                            field.name, a_str, e_str
1741                        ));
1742                    }
1743                }
1744            }
1745        }
1746        if details.is_empty() {
1747            None
1748        } else {
1749            Some(details.join("; "))
1750        }
1751    } else {
1752        None
1753    }
1754}
1755
1756pub fn format_type(ty: &TypeExpr) -> String {
1757    match ty {
1758        TypeExpr::Named(n) => n.clone(),
1759        TypeExpr::Union(types) => types
1760            .iter()
1761            .map(format_type)
1762            .collect::<Vec<_>>()
1763            .join(" | "),
1764        TypeExpr::Shape(fields) => {
1765            let inner: Vec<String> = fields
1766                .iter()
1767                .map(|f| {
1768                    let opt = if f.optional { "?" } else { "" };
1769                    format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
1770                })
1771                .collect();
1772            format!("{{{}}}", inner.join(", "))
1773        }
1774        TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
1775        TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
1776        TypeExpr::FnType {
1777            params,
1778            return_type,
1779        } => {
1780            let params_str = params
1781                .iter()
1782                .map(format_type)
1783                .collect::<Vec<_>>()
1784                .join(", ");
1785            format!("fn({}) -> {}", params_str, format_type(return_type))
1786        }
1787    }
1788}
1789
1790#[cfg(test)]
1791mod tests {
1792    use super::*;
1793    use crate::Parser;
1794    use harn_lexer::Lexer;
1795
1796    fn check_source(source: &str) -> Vec<TypeDiagnostic> {
1797        let mut lexer = Lexer::new(source);
1798        let tokens = lexer.tokenize().unwrap();
1799        let mut parser = Parser::new(tokens);
1800        let program = parser.parse().unwrap();
1801        TypeChecker::new().check(&program)
1802    }
1803
1804    fn errors(source: &str) -> Vec<String> {
1805        check_source(source)
1806            .into_iter()
1807            .filter(|d| d.severity == DiagnosticSeverity::Error)
1808            .map(|d| d.message)
1809            .collect()
1810    }
1811
1812    #[test]
1813    fn test_no_errors_for_untyped_code() {
1814        let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
1815        assert!(errs.is_empty());
1816    }
1817
1818    #[test]
1819    fn test_correct_typed_let() {
1820        let errs = errors("pipeline t(task) { let x: int = 42 }");
1821        assert!(errs.is_empty());
1822    }
1823
1824    #[test]
1825    fn test_type_mismatch_let() {
1826        let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
1827        assert_eq!(errs.len(), 1);
1828        assert!(errs[0].contains("Type mismatch"));
1829        assert!(errs[0].contains("int"));
1830        assert!(errs[0].contains("string"));
1831    }
1832
1833    #[test]
1834    fn test_correct_typed_fn() {
1835        let errs = errors(
1836            "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
1837        );
1838        assert!(errs.is_empty());
1839    }
1840
1841    #[test]
1842    fn test_fn_arg_type_mismatch() {
1843        let errs = errors(
1844            r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
1845add("hello", 2) }"#,
1846        );
1847        assert_eq!(errs.len(), 1);
1848        assert!(errs[0].contains("Argument 1"));
1849        assert!(errs[0].contains("expected int"));
1850    }
1851
1852    #[test]
1853    fn test_return_type_mismatch() {
1854        let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
1855        assert_eq!(errs.len(), 1);
1856        assert!(errs[0].contains("Return type mismatch"));
1857    }
1858
1859    #[test]
1860    fn test_union_type_compatible() {
1861        let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
1862        assert!(errs.is_empty());
1863    }
1864
1865    #[test]
1866    fn test_union_type_mismatch() {
1867        let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
1868        assert_eq!(errs.len(), 1);
1869        assert!(errs[0].contains("Type mismatch"));
1870    }
1871
1872    #[test]
1873    fn test_type_inference_propagation() {
1874        let errs = errors(
1875            r#"pipeline t(task) {
1876  fn add(a: int, b: int) -> int { return a + b }
1877  let result: string = add(1, 2)
1878}"#,
1879        );
1880        assert_eq!(errs.len(), 1);
1881        assert!(errs[0].contains("Type mismatch"));
1882        assert!(errs[0].contains("string"));
1883        assert!(errs[0].contains("int"));
1884    }
1885
1886    #[test]
1887    fn test_builtin_return_type_inference() {
1888        let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
1889        assert_eq!(errs.len(), 1);
1890        assert!(errs[0].contains("string"));
1891        assert!(errs[0].contains("int"));
1892    }
1893
1894    #[test]
1895    fn test_binary_op_type_inference() {
1896        let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
1897        assert_eq!(errs.len(), 1);
1898    }
1899
1900    #[test]
1901    fn test_comparison_returns_bool() {
1902        let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
1903        assert!(errs.is_empty());
1904    }
1905
1906    #[test]
1907    fn test_int_float_promotion() {
1908        let errs = errors("pipeline t(task) { let x: float = 42 }");
1909        assert!(errs.is_empty());
1910    }
1911
1912    #[test]
1913    fn test_untyped_code_no_errors() {
1914        let errs = errors(
1915            r#"pipeline t(task) {
1916  fn process(data) {
1917    let result = data + " processed"
1918    return result
1919  }
1920  log(process("hello"))
1921}"#,
1922        );
1923        assert!(errs.is_empty());
1924    }
1925
1926    #[test]
1927    fn test_type_alias() {
1928        let errs = errors(
1929            r#"pipeline t(task) {
1930  type Name = string
1931  let x: Name = "hello"
1932}"#,
1933        );
1934        assert!(errs.is_empty());
1935    }
1936
1937    #[test]
1938    fn test_type_alias_mismatch() {
1939        let errs = errors(
1940            r#"pipeline t(task) {
1941  type Name = string
1942  let x: Name = 42
1943}"#,
1944        );
1945        assert_eq!(errs.len(), 1);
1946    }
1947
1948    #[test]
1949    fn test_assignment_type_check() {
1950        let errs = errors(
1951            r#"pipeline t(task) {
1952  var x: int = 0
1953  x = "hello"
1954}"#,
1955        );
1956        assert_eq!(errs.len(), 1);
1957        assert!(errs[0].contains("cannot assign string"));
1958    }
1959
1960    #[test]
1961    fn test_covariance_int_to_float_in_fn() {
1962        let errs = errors(
1963            "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
1964        );
1965        assert!(errs.is_empty());
1966    }
1967
1968    #[test]
1969    fn test_covariance_return_type() {
1970        let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
1971        assert!(errs.is_empty());
1972    }
1973
1974    #[test]
1975    fn test_no_contravariance_float_to_int() {
1976        let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
1977        assert_eq!(errs.len(), 1);
1978    }
1979
1980    // --- Exhaustiveness checking tests ---
1981
1982    fn warnings(source: &str) -> Vec<String> {
1983        check_source(source)
1984            .into_iter()
1985            .filter(|d| d.severity == DiagnosticSeverity::Warning)
1986            .map(|d| d.message)
1987            .collect()
1988    }
1989
1990    #[test]
1991    fn test_exhaustive_match_no_warning() {
1992        let warns = warnings(
1993            r#"pipeline t(task) {
1994  enum Color { Red, Green, Blue }
1995  let c = Color.Red
1996  match c.variant {
1997    "Red" -> { log("r") }
1998    "Green" -> { log("g") }
1999    "Blue" -> { log("b") }
2000  }
2001}"#,
2002        );
2003        let exhaustive_warns: Vec<_> = warns
2004            .iter()
2005            .filter(|w| w.contains("Non-exhaustive"))
2006            .collect();
2007        assert!(exhaustive_warns.is_empty());
2008    }
2009
2010    #[test]
2011    fn test_non_exhaustive_match_warning() {
2012        let warns = warnings(
2013            r#"pipeline t(task) {
2014  enum Color { Red, Green, Blue }
2015  let c = Color.Red
2016  match c.variant {
2017    "Red" -> { log("r") }
2018    "Green" -> { log("g") }
2019  }
2020}"#,
2021        );
2022        let exhaustive_warns: Vec<_> = warns
2023            .iter()
2024            .filter(|w| w.contains("Non-exhaustive"))
2025            .collect();
2026        assert_eq!(exhaustive_warns.len(), 1);
2027        assert!(exhaustive_warns[0].contains("Blue"));
2028    }
2029
2030    #[test]
2031    fn test_non_exhaustive_multiple_missing() {
2032        let warns = warnings(
2033            r#"pipeline t(task) {
2034  enum Status { Active, Inactive, Pending }
2035  let s = Status.Active
2036  match s.variant {
2037    "Active" -> { log("a") }
2038  }
2039}"#,
2040        );
2041        let exhaustive_warns: Vec<_> = warns
2042            .iter()
2043            .filter(|w| w.contains("Non-exhaustive"))
2044            .collect();
2045        assert_eq!(exhaustive_warns.len(), 1);
2046        assert!(exhaustive_warns[0].contains("Inactive"));
2047        assert!(exhaustive_warns[0].contains("Pending"));
2048    }
2049
2050    #[test]
2051    fn test_enum_construct_type_inference() {
2052        let errs = errors(
2053            r#"pipeline t(task) {
2054  enum Color { Red, Green, Blue }
2055  let c: Color = Color.Red
2056}"#,
2057        );
2058        assert!(errs.is_empty());
2059    }
2060
2061    // --- Type narrowing tests ---
2062
2063    #[test]
2064    fn test_nil_coalescing_strips_nil() {
2065        // After ??, nil should be stripped from the type
2066        let errs = errors(
2067            r#"pipeline t(task) {
2068  let x: string | nil = nil
2069  let y: string = x ?? "default"
2070}"#,
2071        );
2072        assert!(errs.is_empty());
2073    }
2074
2075    #[test]
2076    fn test_shape_mismatch_detail_missing_field() {
2077        let errs = errors(
2078            r#"pipeline t(task) {
2079  let x: {name: string, age: int} = {name: "hello"}
2080}"#,
2081        );
2082        assert_eq!(errs.len(), 1);
2083        assert!(
2084            errs[0].contains("missing field 'age'"),
2085            "expected detail about missing field, got: {}",
2086            errs[0]
2087        );
2088    }
2089
2090    #[test]
2091    fn test_shape_mismatch_detail_wrong_type() {
2092        let errs = errors(
2093            r#"pipeline t(task) {
2094  let x: {name: string, age: int} = {name: 42, age: 10}
2095}"#,
2096        );
2097        assert_eq!(errs.len(), 1);
2098        assert!(
2099            errs[0].contains("field 'name' has type int, expected string"),
2100            "expected detail about wrong type, got: {}",
2101            errs[0]
2102        );
2103    }
2104
2105    // --- Match pattern type validation tests ---
2106
2107    #[test]
2108    fn test_match_pattern_string_against_int() {
2109        let warns = warnings(
2110            r#"pipeline t(task) {
2111  let x: int = 42
2112  match x {
2113    "hello" -> { log("bad") }
2114    42 -> { log("ok") }
2115  }
2116}"#,
2117        );
2118        let pattern_warns: Vec<_> = warns
2119            .iter()
2120            .filter(|w| w.contains("Match pattern type mismatch"))
2121            .collect();
2122        assert_eq!(pattern_warns.len(), 1);
2123        assert!(pattern_warns[0].contains("matching int against string literal"));
2124    }
2125
2126    #[test]
2127    fn test_match_pattern_int_against_string() {
2128        let warns = warnings(
2129            r#"pipeline t(task) {
2130  let x: string = "hello"
2131  match x {
2132    42 -> { log("bad") }
2133    "hello" -> { log("ok") }
2134  }
2135}"#,
2136        );
2137        let pattern_warns: Vec<_> = warns
2138            .iter()
2139            .filter(|w| w.contains("Match pattern type mismatch"))
2140            .collect();
2141        assert_eq!(pattern_warns.len(), 1);
2142        assert!(pattern_warns[0].contains("matching string against int literal"));
2143    }
2144
2145    #[test]
2146    fn test_match_pattern_bool_against_int() {
2147        let warns = warnings(
2148            r#"pipeline t(task) {
2149  let x: int = 42
2150  match x {
2151    true -> { log("bad") }
2152    42 -> { log("ok") }
2153  }
2154}"#,
2155        );
2156        let pattern_warns: Vec<_> = warns
2157            .iter()
2158            .filter(|w| w.contains("Match pattern type mismatch"))
2159            .collect();
2160        assert_eq!(pattern_warns.len(), 1);
2161        assert!(pattern_warns[0].contains("matching int against bool literal"));
2162    }
2163
2164    #[test]
2165    fn test_match_pattern_float_against_string() {
2166        let warns = warnings(
2167            r#"pipeline t(task) {
2168  let x: string = "hello"
2169  match x {
2170    3.14 -> { log("bad") }
2171    "hello" -> { log("ok") }
2172  }
2173}"#,
2174        );
2175        let pattern_warns: Vec<_> = warns
2176            .iter()
2177            .filter(|w| w.contains("Match pattern type mismatch"))
2178            .collect();
2179        assert_eq!(pattern_warns.len(), 1);
2180        assert!(pattern_warns[0].contains("matching string against float literal"));
2181    }
2182
2183    #[test]
2184    fn test_match_pattern_int_against_float_ok() {
2185        // int and float are compatible for match patterns
2186        let warns = warnings(
2187            r#"pipeline t(task) {
2188  let x: float = 3.14
2189  match x {
2190    42 -> { log("ok") }
2191    _ -> { log("default") }
2192  }
2193}"#,
2194        );
2195        let pattern_warns: Vec<_> = warns
2196            .iter()
2197            .filter(|w| w.contains("Match pattern type mismatch"))
2198            .collect();
2199        assert!(pattern_warns.is_empty());
2200    }
2201
2202    #[test]
2203    fn test_match_pattern_float_against_int_ok() {
2204        // float and int are compatible for match patterns
2205        let warns = warnings(
2206            r#"pipeline t(task) {
2207  let x: int = 42
2208  match x {
2209    3.14 -> { log("close") }
2210    _ -> { log("default") }
2211  }
2212}"#,
2213        );
2214        let pattern_warns: Vec<_> = warns
2215            .iter()
2216            .filter(|w| w.contains("Match pattern type mismatch"))
2217            .collect();
2218        assert!(pattern_warns.is_empty());
2219    }
2220
2221    #[test]
2222    fn test_match_pattern_correct_types_no_warning() {
2223        let warns = warnings(
2224            r#"pipeline t(task) {
2225  let x: int = 42
2226  match x {
2227    1 -> { log("one") }
2228    2 -> { log("two") }
2229    _ -> { log("other") }
2230  }
2231}"#,
2232        );
2233        let pattern_warns: Vec<_> = warns
2234            .iter()
2235            .filter(|w| w.contains("Match pattern type mismatch"))
2236            .collect();
2237        assert!(pattern_warns.is_empty());
2238    }
2239
2240    #[test]
2241    fn test_match_pattern_wildcard_no_warning() {
2242        let warns = warnings(
2243            r#"pipeline t(task) {
2244  let x: int = 42
2245  match x {
2246    _ -> { log("catch all") }
2247  }
2248}"#,
2249        );
2250        let pattern_warns: Vec<_> = warns
2251            .iter()
2252            .filter(|w| w.contains("Match pattern type mismatch"))
2253            .collect();
2254        assert!(pattern_warns.is_empty());
2255    }
2256
2257    #[test]
2258    fn test_match_pattern_untyped_no_warning() {
2259        // When value has no known type, no warning should be emitted
2260        let warns = warnings(
2261            r#"pipeline t(task) {
2262  let x = some_unknown_fn()
2263  match x {
2264    "hello" -> { log("string") }
2265    42 -> { log("int") }
2266  }
2267}"#,
2268        );
2269        let pattern_warns: Vec<_> = warns
2270            .iter()
2271            .filter(|w| w.contains("Match pattern type mismatch"))
2272            .collect();
2273        assert!(pattern_warns.is_empty());
2274    }
2275
2276    // --- Interface constraint type checking tests ---
2277
2278    fn iface_warns(source: &str) -> Vec<String> {
2279        warnings(source)
2280            .into_iter()
2281            .filter(|w| w.contains("does not satisfy interface"))
2282            .collect()
2283    }
2284
2285    #[test]
2286    fn test_interface_constraint_return_type_mismatch() {
2287        let warns = iface_warns(
2288            r#"pipeline t(task) {
2289  interface Sizable {
2290    fn size(self) -> int
2291  }
2292  struct Box { width: int }
2293  impl Box {
2294    fn size(self) -> string { return "nope" }
2295  }
2296  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2297  measure(Box({width: 3}))
2298}"#,
2299        );
2300        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2301        assert!(
2302            warns[0].contains("method 'size' returns 'string', expected 'int'"),
2303            "unexpected message: {}",
2304            warns[0]
2305        );
2306    }
2307
2308    #[test]
2309    fn test_interface_constraint_param_type_mismatch() {
2310        let warns = iface_warns(
2311            r#"pipeline t(task) {
2312  interface Processor {
2313    fn process(self, x: int) -> string
2314  }
2315  struct MyProc { name: string }
2316  impl MyProc {
2317    fn process(self, x: string) -> string { return x }
2318  }
2319  fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
2320  run_proc(MyProc({name: "a"}))
2321}"#,
2322        );
2323        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2324        assert!(
2325            warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
2326            "unexpected message: {}",
2327            warns[0]
2328        );
2329    }
2330
2331    #[test]
2332    fn test_interface_constraint_missing_method() {
2333        let warns = iface_warns(
2334            r#"pipeline t(task) {
2335  interface Sizable {
2336    fn size(self) -> int
2337  }
2338  struct Box { width: int }
2339  impl Box {
2340    fn area(self) -> int { return self.width }
2341  }
2342  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2343  measure(Box({width: 3}))
2344}"#,
2345        );
2346        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2347        assert!(
2348            warns[0].contains("missing method 'size'"),
2349            "unexpected message: {}",
2350            warns[0]
2351        );
2352    }
2353
2354    #[test]
2355    fn test_interface_constraint_param_count_mismatch() {
2356        let warns = iface_warns(
2357            r#"pipeline t(task) {
2358  interface Doubler {
2359    fn double(self, x: int) -> int
2360  }
2361  struct Bad { v: int }
2362  impl Bad {
2363    fn double(self) -> int { return self.v * 2 }
2364  }
2365  fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
2366  run_double(Bad({v: 5}))
2367}"#,
2368        );
2369        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2370        assert!(
2371            warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
2372            "unexpected message: {}",
2373            warns[0]
2374        );
2375    }
2376
2377    #[test]
2378    fn test_interface_constraint_satisfied() {
2379        let warns = iface_warns(
2380            r#"pipeline t(task) {
2381  interface Sizable {
2382    fn size(self) -> int
2383  }
2384  struct Box { width: int, height: int }
2385  impl Box {
2386    fn size(self) -> int { return self.width * self.height }
2387  }
2388  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2389  measure(Box({width: 3, height: 4}))
2390}"#,
2391        );
2392        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
2393    }
2394
2395    #[test]
2396    fn test_interface_constraint_untyped_impl_compatible() {
2397        // Gradual typing: untyped impl return should not trigger warning
2398        let warns = iface_warns(
2399            r#"pipeline t(task) {
2400  interface Sizable {
2401    fn size(self) -> int
2402  }
2403  struct Box { width: int }
2404  impl Box {
2405    fn size(self) { return self.width }
2406  }
2407  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2408  measure(Box({width: 3}))
2409}"#,
2410        );
2411        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
2412    }
2413
2414    #[test]
2415    fn test_interface_constraint_int_float_covariance() {
2416        // int is compatible with float (covariance)
2417        let warns = iface_warns(
2418            r#"pipeline t(task) {
2419  interface Measurable {
2420    fn value(self) -> float
2421  }
2422  struct Gauge { v: int }
2423  impl Gauge {
2424    fn value(self) -> int { return self.v }
2425  }
2426  fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
2427  read_val(Gauge({v: 42}))
2428}"#,
2429        );
2430        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
2431    }
2432}