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