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