Skip to main content

harn_parser/
typechecker.rs

1use std::collections::BTreeMap;
2
3use crate::ast::*;
4use crate::builtin_signatures;
5use harn_lexer::Span;
6
7/// A diagnostic produced by the type checker.
8#[derive(Debug, Clone)]
9pub struct TypeDiagnostic {
10    pub message: String,
11    pub severity: DiagnosticSeverity,
12    pub span: Option<Span>,
13    pub help: Option<String>,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum DiagnosticSeverity {
18    Error,
19    Warning,
20}
21
22/// Inferred type of an expression. None means unknown/untyped (gradual typing).
23type InferredType = Option<TypeExpr>;
24
25/// Scope for tracking variable types.
26#[derive(Debug, Clone)]
27struct TypeScope {
28    /// Variable name → inferred type.
29    vars: BTreeMap<String, InferredType>,
30    /// Function name → (param types, return type).
31    functions: BTreeMap<String, FnSignature>,
32    /// Named type aliases.
33    type_aliases: BTreeMap<String, TypeExpr>,
34    /// Enum declarations: name → variant names.
35    enums: BTreeMap<String, Vec<String>>,
36    /// Interface declarations: name → method signatures.
37    interfaces: BTreeMap<String, Vec<InterfaceMethod>>,
38    /// Struct declarations: name → field types.
39    structs: BTreeMap<String, Vec<(String, InferredType)>>,
40    /// Impl block methods: type_name → method signatures.
41    impl_methods: BTreeMap<String, Vec<ImplMethodSig>>,
42    /// Generic type parameter names in scope (treated as compatible with any type).
43    generic_type_params: std::collections::BTreeSet<String>,
44    /// Where-clause constraints: type_param → interface_bound.
45    /// Used for definition-site checking of generic function bodies.
46    where_constraints: BTreeMap<String, String>,
47    /// Variables declared with `var` (mutable). Variables not in this set
48    /// are immutable (`let`, function params, loop vars, etc.).
49    mutable_vars: std::collections::BTreeSet<String>,
50    /// Variables that have been narrowed by flow-sensitive refinement.
51    /// Maps var name → pre-narrowing type (used to restore on reassignment).
52    narrowed_vars: BTreeMap<String, InferredType>,
53    parent: Option<Box<TypeScope>>,
54}
55
56/// Method signature extracted from an impl block (for interface checking).
57#[derive(Debug, Clone)]
58struct ImplMethodSig {
59    name: String,
60    /// Number of parameters excluding `self`.
61    param_count: usize,
62    /// Parameter types (excluding `self`), None means untyped.
63    param_types: Vec<Option<TypeExpr>>,
64    /// Return type, None means untyped.
65    return_type: Option<TypeExpr>,
66}
67
68#[derive(Debug, Clone)]
69struct FnSignature {
70    params: Vec<(String, InferredType)>,
71    return_type: InferredType,
72    /// Generic type parameter names declared on the function.
73    type_param_names: Vec<String>,
74    /// Number of required parameters (those without defaults).
75    required_params: usize,
76    /// Where-clause constraints: (type_param_name, interface_bound).
77    where_clauses: Vec<(String, String)>,
78    /// True if the last parameter is a rest parameter.
79    has_rest: bool,
80}
81
82impl TypeScope {
83    fn new() -> Self {
84        Self {
85            vars: BTreeMap::new(),
86            functions: BTreeMap::new(),
87            type_aliases: BTreeMap::new(),
88            enums: BTreeMap::new(),
89            interfaces: BTreeMap::new(),
90            structs: BTreeMap::new(),
91            impl_methods: BTreeMap::new(),
92            generic_type_params: std::collections::BTreeSet::new(),
93            where_constraints: BTreeMap::new(),
94            mutable_vars: std::collections::BTreeSet::new(),
95            narrowed_vars: BTreeMap::new(),
96            parent: None,
97        }
98    }
99
100    fn child(&self) -> Self {
101        Self {
102            vars: BTreeMap::new(),
103            functions: BTreeMap::new(),
104            type_aliases: BTreeMap::new(),
105            enums: BTreeMap::new(),
106            interfaces: BTreeMap::new(),
107            structs: BTreeMap::new(),
108            impl_methods: BTreeMap::new(),
109            generic_type_params: std::collections::BTreeSet::new(),
110            where_constraints: BTreeMap::new(),
111            mutable_vars: std::collections::BTreeSet::new(),
112            narrowed_vars: BTreeMap::new(),
113            parent: Some(Box::new(self.clone())),
114        }
115    }
116
117    fn get_var(&self, name: &str) -> Option<&InferredType> {
118        self.vars
119            .get(name)
120            .or_else(|| self.parent.as_ref()?.get_var(name))
121    }
122
123    fn get_fn(&self, name: &str) -> Option<&FnSignature> {
124        self.functions
125            .get(name)
126            .or_else(|| self.parent.as_ref()?.get_fn(name))
127    }
128
129    fn resolve_type(&self, name: &str) -> Option<&TypeExpr> {
130        self.type_aliases
131            .get(name)
132            .or_else(|| self.parent.as_ref()?.resolve_type(name))
133    }
134
135    fn is_generic_type_param(&self, name: &str) -> bool {
136        self.generic_type_params.contains(name)
137            || self
138                .parent
139                .as_ref()
140                .is_some_and(|p| p.is_generic_type_param(name))
141    }
142
143    fn get_where_constraint(&self, type_param: &str) -> Option<&str> {
144        self.where_constraints
145            .get(type_param)
146            .map(|s| s.as_str())
147            .or_else(|| {
148                self.parent
149                    .as_ref()
150                    .and_then(|p| p.get_where_constraint(type_param))
151            })
152    }
153
154    fn get_enum(&self, name: &str) -> Option<&Vec<String>> {
155        self.enums
156            .get(name)
157            .or_else(|| self.parent.as_ref()?.get_enum(name))
158    }
159
160    fn get_interface(&self, name: &str) -> Option<&Vec<InterfaceMethod>> {
161        self.interfaces
162            .get(name)
163            .or_else(|| self.parent.as_ref()?.get_interface(name))
164    }
165
166    fn get_struct(&self, name: &str) -> Option<&Vec<(String, InferredType)>> {
167        self.structs
168            .get(name)
169            .or_else(|| self.parent.as_ref()?.get_struct(name))
170    }
171
172    fn get_impl_methods(&self, name: &str) -> Option<&Vec<ImplMethodSig>> {
173        self.impl_methods
174            .get(name)
175            .or_else(|| self.parent.as_ref()?.get_impl_methods(name))
176    }
177
178    fn define_var(&mut self, name: &str, ty: InferredType) {
179        self.vars.insert(name.to_string(), ty);
180    }
181
182    fn define_var_mutable(&mut self, name: &str, ty: InferredType) {
183        self.vars.insert(name.to_string(), ty);
184        self.mutable_vars.insert(name.to_string());
185    }
186
187    /// Check if a variable is mutable (declared with `var`).
188    fn is_mutable(&self, name: &str) -> bool {
189        self.mutable_vars.contains(name) || self.parent.as_ref().is_some_and(|p| p.is_mutable(name))
190    }
191
192    fn define_fn(&mut self, name: &str, sig: FnSignature) {
193        self.functions.insert(name.to_string(), sig);
194    }
195}
196
197/// Bidirectional type refinements extracted from a condition.
198/// Each path contains a list of (variable_name, narrowed_type) pairs.
199#[derive(Debug, Clone, Default)]
200struct Refinements {
201    /// Narrowings when the condition evaluates to true/truthy.
202    truthy: Vec<(String, InferredType)>,
203    /// Narrowings when the condition evaluates to false/falsy.
204    falsy: Vec<(String, InferredType)>,
205}
206
207impl Refinements {
208    fn empty() -> Self {
209        Self::default()
210    }
211
212    /// Swap truthy and falsy (used for negation).
213    fn inverted(self) -> Self {
214        Self {
215            truthy: self.falsy,
216            falsy: self.truthy,
217        }
218    }
219}
220
221/// Known return types for builtin functions. Delegates to the shared
222/// [`builtin_signatures`] registry — see that module for the full table.
223fn builtin_return_type(name: &str) -> InferredType {
224    builtin_signatures::builtin_return_type(name)
225}
226
227/// Check if a name is a known builtin. Delegates to the shared
228/// [`builtin_signatures`] registry.
229fn is_builtin(name: &str) -> bool {
230    builtin_signatures::is_builtin(name)
231}
232
233/// The static type checker.
234pub struct TypeChecker {
235    diagnostics: Vec<TypeDiagnostic>,
236    scope: TypeScope,
237}
238
239impl TypeChecker {
240    pub fn new() -> Self {
241        Self {
242            diagnostics: Vec::new(),
243            scope: TypeScope::new(),
244        }
245    }
246
247    /// Check a program and return diagnostics.
248    pub fn check(mut self, program: &[SNode]) -> Vec<TypeDiagnostic> {
249        // First pass: register type and enum declarations into root scope
250        Self::register_declarations_into(&mut self.scope, program);
251
252        // Also scan pipeline bodies for declarations
253        for snode in program {
254            if let Node::Pipeline { body, .. } = &snode.node {
255                Self::register_declarations_into(&mut self.scope, body);
256            }
257        }
258
259        // Check each top-level node
260        for snode in program {
261            match &snode.node {
262                Node::Pipeline { params, body, .. } => {
263                    let mut child = self.scope.child();
264                    for p in params {
265                        child.define_var(p, None);
266                    }
267                    self.check_block(body, &mut child);
268                }
269                Node::FnDecl {
270                    name,
271                    type_params,
272                    params,
273                    return_type,
274                    where_clauses,
275                    body,
276                    ..
277                } => {
278                    let required_params =
279                        params.iter().filter(|p| p.default_value.is_none()).count();
280                    let sig = FnSignature {
281                        params: params
282                            .iter()
283                            .map(|p| (p.name.clone(), p.type_expr.clone()))
284                            .collect(),
285                        return_type: return_type.clone(),
286                        type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
287                        required_params,
288                        where_clauses: where_clauses
289                            .iter()
290                            .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
291                            .collect(),
292                        has_rest: params.last().is_some_and(|p| p.rest),
293                    };
294                    self.scope.define_fn(name, sig);
295                    self.check_fn_body(type_params, params, return_type, body, where_clauses);
296                }
297                _ => {
298                    let mut scope = self.scope.clone();
299                    self.check_node(snode, &mut scope);
300                    // Merge any new definitions back into the top-level scope
301                    for (name, ty) in scope.vars {
302                        self.scope.vars.entry(name).or_insert(ty);
303                    }
304                    for name in scope.mutable_vars {
305                        self.scope.mutable_vars.insert(name);
306                    }
307                }
308            }
309        }
310
311        self.diagnostics
312    }
313
314    /// Register type, enum, interface, and struct declarations from AST nodes into a scope.
315    fn register_declarations_into(scope: &mut TypeScope, nodes: &[SNode]) {
316        for snode in nodes {
317            match &snode.node {
318                Node::TypeDecl { name, type_expr } => {
319                    scope.type_aliases.insert(name.clone(), type_expr.clone());
320                }
321                Node::EnumDecl { name, variants, .. } => {
322                    let variant_names: Vec<String> =
323                        variants.iter().map(|v| v.name.clone()).collect();
324                    scope.enums.insert(name.clone(), variant_names);
325                }
326                Node::InterfaceDecl { name, methods, .. } => {
327                    scope.interfaces.insert(name.clone(), methods.clone());
328                }
329                Node::StructDecl { name, fields, .. } => {
330                    let field_types: Vec<(String, InferredType)> = fields
331                        .iter()
332                        .map(|f| (f.name.clone(), f.type_expr.clone()))
333                        .collect();
334                    scope.structs.insert(name.clone(), field_types);
335                }
336                Node::ImplBlock {
337                    type_name, methods, ..
338                } => {
339                    let sigs: Vec<ImplMethodSig> = methods
340                        .iter()
341                        .filter_map(|m| {
342                            if let Node::FnDecl {
343                                name,
344                                params,
345                                return_type,
346                                ..
347                            } = &m.node
348                            {
349                                let non_self: Vec<_> =
350                                    params.iter().filter(|p| p.name != "self").collect();
351                                let param_count = non_self.len();
352                                let param_types: Vec<Option<TypeExpr>> =
353                                    non_self.iter().map(|p| p.type_expr.clone()).collect();
354                                Some(ImplMethodSig {
355                                    name: name.clone(),
356                                    param_count,
357                                    param_types,
358                                    return_type: return_type.clone(),
359                                })
360                            } else {
361                                None
362                            }
363                        })
364                        .collect();
365                    scope.impl_methods.insert(type_name.clone(), sigs);
366                }
367                _ => {}
368            }
369        }
370    }
371
372    fn check_block(&mut self, stmts: &[SNode], scope: &mut TypeScope) {
373        for stmt in stmts {
374            self.check_node(stmt, scope);
375        }
376    }
377
378    /// Define variables from a destructuring pattern in the given scope (as unknown type).
379    fn define_pattern_vars(pattern: &BindingPattern, scope: &mut TypeScope, mutable: bool) {
380        let define = |scope: &mut TypeScope, name: &str| {
381            if mutable {
382                scope.define_var_mutable(name, None);
383            } else {
384                scope.define_var(name, None);
385            }
386        };
387        match pattern {
388            BindingPattern::Identifier(name) => {
389                define(scope, name);
390            }
391            BindingPattern::Dict(fields) => {
392                for field in fields {
393                    let name = field.alias.as_deref().unwrap_or(&field.key);
394                    define(scope, name);
395                }
396            }
397            BindingPattern::List(elements) => {
398                for elem in elements {
399                    define(scope, &elem.name);
400                }
401            }
402        }
403    }
404
405    fn check_node(&mut self, snode: &SNode, scope: &mut TypeScope) {
406        let span = snode.span;
407        match &snode.node {
408            Node::LetBinding {
409                pattern,
410                type_ann,
411                value,
412            } => {
413                let inferred = self.infer_type(value, scope);
414                if let BindingPattern::Identifier(name) = pattern {
415                    if let Some(expected) = type_ann {
416                        if let Some(actual) = &inferred {
417                            if !self.types_compatible(expected, actual, scope) {
418                                let mut msg = format!(
419                                    "Type mismatch: '{}' declared as {}, but assigned {}",
420                                    name,
421                                    format_type(expected),
422                                    format_type(actual)
423                                );
424                                if let Some(detail) = shape_mismatch_detail(expected, actual) {
425                                    msg.push_str(&format!(" ({})", detail));
426                                }
427                                self.error_at(msg, span);
428                            }
429                        }
430                    }
431                    let ty = type_ann.clone().or(inferred);
432                    scope.define_var(name, ty);
433                } else {
434                    Self::define_pattern_vars(pattern, scope, false);
435                }
436            }
437
438            Node::VarBinding {
439                pattern,
440                type_ann,
441                value,
442            } => {
443                let inferred = self.infer_type(value, scope);
444                if let BindingPattern::Identifier(name) = pattern {
445                    if let Some(expected) = type_ann {
446                        if let Some(actual) = &inferred {
447                            if !self.types_compatible(expected, actual, scope) {
448                                let mut msg = format!(
449                                    "Type mismatch: '{}' declared as {}, but assigned {}",
450                                    name,
451                                    format_type(expected),
452                                    format_type(actual)
453                                );
454                                if let Some(detail) = shape_mismatch_detail(expected, actual) {
455                                    msg.push_str(&format!(" ({})", detail));
456                                }
457                                self.error_at(msg, span);
458                            }
459                        }
460                    }
461                    let ty = type_ann.clone().or(inferred);
462                    scope.define_var_mutable(name, ty);
463                } else {
464                    Self::define_pattern_vars(pattern, scope, true);
465                }
466            }
467
468            Node::FnDecl {
469                name,
470                type_params,
471                params,
472                return_type,
473                where_clauses,
474                body,
475                ..
476            } => {
477                let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
478                let sig = FnSignature {
479                    params: params
480                        .iter()
481                        .map(|p| (p.name.clone(), p.type_expr.clone()))
482                        .collect(),
483                    return_type: return_type.clone(),
484                    type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
485                    required_params,
486                    where_clauses: where_clauses
487                        .iter()
488                        .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
489                        .collect(),
490                    has_rest: params.last().is_some_and(|p| p.rest),
491                };
492                scope.define_fn(name, sig.clone());
493                scope.define_var(name, None);
494                self.check_fn_body(type_params, params, return_type, body, where_clauses);
495            }
496
497            Node::ToolDecl {
498                name,
499                params,
500                return_type,
501                body,
502                ..
503            } => {
504                // Register the tool like a function for type checking purposes
505                let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
506                let sig = FnSignature {
507                    params: params
508                        .iter()
509                        .map(|p| (p.name.clone(), p.type_expr.clone()))
510                        .collect(),
511                    return_type: return_type.clone(),
512                    type_param_names: Vec::new(),
513                    required_params,
514                    where_clauses: Vec::new(),
515                    has_rest: params.last().is_some_and(|p| p.rest),
516                };
517                scope.define_fn(name, sig);
518                scope.define_var(name, None);
519                self.check_fn_body(&[], params, return_type, body, &[]);
520            }
521
522            Node::FunctionCall { name, args } => {
523                self.check_call(name, args, scope, span);
524            }
525
526            Node::IfElse {
527                condition,
528                then_body,
529                else_body,
530            } => {
531                self.check_node(condition, scope);
532                let refs = Self::extract_refinements(condition, scope);
533
534                let mut then_scope = scope.child();
535                apply_refinements(&mut then_scope, &refs.truthy);
536                self.check_block(then_body, &mut then_scope);
537
538                if let Some(else_body) = else_body {
539                    let mut else_scope = scope.child();
540                    apply_refinements(&mut else_scope, &refs.falsy);
541                    self.check_block(else_body, &mut else_scope);
542
543                    // Post-branch narrowing: if one branch definitely exits,
544                    // apply the other branch's refinements to the outer scope
545                    if Self::block_definitely_exits(then_body)
546                        && !Self::block_definitely_exits(else_body)
547                    {
548                        apply_refinements(scope, &refs.falsy);
549                    } else if Self::block_definitely_exits(else_body)
550                        && !Self::block_definitely_exits(then_body)
551                    {
552                        apply_refinements(scope, &refs.truthy);
553                    }
554                } else {
555                    // No else: if then-body always exits, apply falsy after
556                    if Self::block_definitely_exits(then_body) {
557                        apply_refinements(scope, &refs.falsy);
558                    }
559                }
560            }
561
562            Node::ForIn {
563                pattern,
564                iterable,
565                body,
566            } => {
567                self.check_node(iterable, scope);
568                let mut loop_scope = scope.child();
569                if let BindingPattern::Identifier(variable) = pattern {
570                    // Infer loop variable type from iterable
571                    let elem_type = match self.infer_type(iterable, scope) {
572                        Some(TypeExpr::List(inner)) => Some(*inner),
573                        Some(TypeExpr::Named(n)) if n == "string" => {
574                            Some(TypeExpr::Named("string".into()))
575                        }
576                        _ => None,
577                    };
578                    loop_scope.define_var(variable, elem_type);
579                } else {
580                    Self::define_pattern_vars(pattern, &mut loop_scope, false);
581                }
582                self.check_block(body, &mut loop_scope);
583            }
584
585            Node::WhileLoop { condition, body } => {
586                self.check_node(condition, scope);
587                let refs = Self::extract_refinements(condition, scope);
588                let mut loop_scope = scope.child();
589                apply_refinements(&mut loop_scope, &refs.truthy);
590                self.check_block(body, &mut loop_scope);
591            }
592
593            Node::RequireStmt { condition, message } => {
594                self.check_node(condition, scope);
595                if let Some(message) = message {
596                    self.check_node(message, scope);
597                }
598            }
599
600            Node::TryCatch {
601                body,
602                error_var,
603                catch_body,
604                finally_body,
605                ..
606            } => {
607                let mut try_scope = scope.child();
608                self.check_block(body, &mut try_scope);
609                let mut catch_scope = scope.child();
610                if let Some(var) = error_var {
611                    catch_scope.define_var(var, None);
612                }
613                self.check_block(catch_body, &mut catch_scope);
614                if let Some(fb) = finally_body {
615                    let mut finally_scope = scope.child();
616                    self.check_block(fb, &mut finally_scope);
617                }
618            }
619
620            Node::TryExpr { body } => {
621                let mut try_scope = scope.child();
622                self.check_block(body, &mut try_scope);
623            }
624
625            Node::ReturnStmt {
626                value: Some(val), ..
627            } => {
628                self.check_node(val, scope);
629            }
630
631            Node::Assignment {
632                target, value, op, ..
633            } => {
634                self.check_node(value, scope);
635                if let Node::Identifier(name) = &target.node {
636                    // Compile-time immutability check
637                    if scope.get_var(name).is_some() && !scope.is_mutable(name) {
638                        self.warning_at(
639                            format!(
640                                "Cannot assign to '{}': variable is immutable (declared with 'let')",
641                                name
642                            ),
643                            span,
644                        );
645                    }
646
647                    if let Some(Some(var_type)) = scope.get_var(name) {
648                        let value_type = self.infer_type(value, scope);
649                        let assigned = if let Some(op) = op {
650                            let var_inferred = scope.get_var(name).cloned().flatten();
651                            infer_binary_op_type(op, &var_inferred, &value_type)
652                        } else {
653                            value_type
654                        };
655                        if let Some(actual) = &assigned {
656                            // Check against the original (pre-narrowing) type if narrowed
657                            let check_type = scope
658                                .narrowed_vars
659                                .get(name)
660                                .and_then(|t| t.as_ref())
661                                .unwrap_or(var_type);
662                            if !self.types_compatible(check_type, actual, scope) {
663                                self.error_at(
664                                    format!(
665                                        "Type mismatch: cannot assign {} to '{}' (declared as {})",
666                                        format_type(actual),
667                                        name,
668                                        format_type(check_type)
669                                    ),
670                                    span,
671                                );
672                            }
673                        }
674                    }
675
676                    // Invalidate narrowing on reassignment: restore original type
677                    if let Some(original) = scope.narrowed_vars.remove(name) {
678                        scope.define_var(name, original);
679                    }
680                }
681            }
682
683            Node::TypeDecl { name, type_expr } => {
684                scope.type_aliases.insert(name.clone(), type_expr.clone());
685            }
686
687            Node::EnumDecl { name, variants, .. } => {
688                let variant_names: Vec<String> = variants.iter().map(|v| v.name.clone()).collect();
689                scope.enums.insert(name.clone(), variant_names);
690            }
691
692            Node::StructDecl { name, fields, .. } => {
693                let field_types: Vec<(String, InferredType)> = fields
694                    .iter()
695                    .map(|f| (f.name.clone(), f.type_expr.clone()))
696                    .collect();
697                scope.structs.insert(name.clone(), field_types);
698            }
699
700            Node::InterfaceDecl { name, methods, .. } => {
701                scope.interfaces.insert(name.clone(), methods.clone());
702            }
703
704            Node::ImplBlock {
705                type_name, methods, ..
706            } => {
707                // Register impl methods for interface satisfaction checking
708                let sigs: Vec<ImplMethodSig> = methods
709                    .iter()
710                    .filter_map(|m| {
711                        if let Node::FnDecl {
712                            name,
713                            params,
714                            return_type,
715                            ..
716                        } = &m.node
717                        {
718                            let non_self: Vec<_> =
719                                params.iter().filter(|p| p.name != "self").collect();
720                            let param_count = non_self.len();
721                            let param_types: Vec<Option<TypeExpr>> =
722                                non_self.iter().map(|p| p.type_expr.clone()).collect();
723                            Some(ImplMethodSig {
724                                name: name.clone(),
725                                param_count,
726                                param_types,
727                                return_type: return_type.clone(),
728                            })
729                        } else {
730                            None
731                        }
732                    })
733                    .collect();
734                scope.impl_methods.insert(type_name.clone(), sigs);
735                for method_sn in methods {
736                    self.check_node(method_sn, scope);
737                }
738            }
739
740            Node::TryOperator { operand } => {
741                self.check_node(operand, scope);
742            }
743
744            Node::MatchExpr { value, arms } => {
745                self.check_node(value, scope);
746                let value_type = self.infer_type(value, scope);
747                for arm in arms {
748                    self.check_node(&arm.pattern, scope);
749                    // Check for incompatible literal pattern types
750                    if let Some(ref vt) = value_type {
751                        let value_type_name = format_type(vt);
752                        let mismatch = match &arm.pattern.node {
753                            Node::StringLiteral(_) => {
754                                !self.types_compatible(vt, &TypeExpr::Named("string".into()), scope)
755                            }
756                            Node::IntLiteral(_) => {
757                                !self.types_compatible(vt, &TypeExpr::Named("int".into()), scope)
758                                    && !self.types_compatible(
759                                        vt,
760                                        &TypeExpr::Named("float".into()),
761                                        scope,
762                                    )
763                            }
764                            Node::FloatLiteral(_) => {
765                                !self.types_compatible(vt, &TypeExpr::Named("float".into()), scope)
766                                    && !self.types_compatible(
767                                        vt,
768                                        &TypeExpr::Named("int".into()),
769                                        scope,
770                                    )
771                            }
772                            Node::BoolLiteral(_) => {
773                                !self.types_compatible(vt, &TypeExpr::Named("bool".into()), scope)
774                            }
775                            _ => false,
776                        };
777                        if mismatch {
778                            let pattern_type = match &arm.pattern.node {
779                                Node::StringLiteral(_) => "string",
780                                Node::IntLiteral(_) => "int",
781                                Node::FloatLiteral(_) => "float",
782                                Node::BoolLiteral(_) => "bool",
783                                _ => unreachable!(),
784                            };
785                            self.warning_at(
786                                format!(
787                                    "Match pattern type mismatch: matching {} against {} literal",
788                                    value_type_name, pattern_type
789                                ),
790                                arm.pattern.span,
791                            );
792                        }
793                    }
794                    let mut arm_scope = scope.child();
795                    // Narrow the matched value's type in each arm
796                    if let Node::Identifier(var_name) = &value.node {
797                        if let Some(Some(TypeExpr::Union(members))) = scope.get_var(var_name) {
798                            let narrowed = match &arm.pattern.node {
799                                Node::NilLiteral => narrow_to_single(members, "nil"),
800                                Node::StringLiteral(_) => narrow_to_single(members, "string"),
801                                Node::IntLiteral(_) => narrow_to_single(members, "int"),
802                                Node::FloatLiteral(_) => narrow_to_single(members, "float"),
803                                Node::BoolLiteral(_) => narrow_to_single(members, "bool"),
804                                _ => None,
805                            };
806                            if let Some(narrowed_type) = narrowed {
807                                arm_scope.define_var(var_name, Some(narrowed_type));
808                            }
809                        }
810                    }
811                    self.check_block(&arm.body, &mut arm_scope);
812                }
813                self.check_match_exhaustiveness(value, arms, scope, span);
814            }
815
816            // Recurse into nested expressions + validate binary op types
817            Node::BinaryOp { op, left, right } => {
818                self.check_node(left, scope);
819                self.check_node(right, scope);
820                // Validate operator/type compatibility
821                let lt = self.infer_type(left, scope);
822                let rt = self.infer_type(right, scope);
823                if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (&lt, &rt) {
824                    match op.as_str() {
825                        "-" | "/" | "%" => {
826                            let numeric = ["int", "float"];
827                            if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
828                                self.warning_at(
829                                    format!(
830                                        "Operator '{op}' may not be valid for types {} and {}",
831                                        l, r
832                                    ),
833                                    span,
834                                );
835                            }
836                        }
837                        "*" => {
838                            let numeric = ["int", "float"];
839                            let is_numeric =
840                                numeric.contains(&l.as_str()) && numeric.contains(&r.as_str());
841                            let is_string_repeat =
842                                (l == "string" && r == "int") || (l == "int" && r == "string");
843                            if !is_numeric && !is_string_repeat {
844                                self.warning_at(
845                                    format!(
846                                        "Operator '{op}' may not be valid for types {} and {}",
847                                        l, r
848                                    ),
849                                    span,
850                                );
851                            }
852                        }
853                        "+" => {
854                            // + is valid for int, float, string, list, dict
855                            let valid = ["int", "float", "string", "list", "dict"];
856                            if !valid.contains(&l.as_str()) && !valid.contains(&r.as_str()) {
857                                self.warning_at(
858                                    format!(
859                                        "Operator '+' may not be valid for types {} and {}",
860                                        l, r
861                                    ),
862                                    span,
863                                );
864                            }
865                        }
866                        _ => {}
867                    }
868                }
869            }
870            Node::UnaryOp { operand, .. } => {
871                self.check_node(operand, scope);
872            }
873            Node::MethodCall {
874                object,
875                method,
876                args,
877                ..
878            }
879            | Node::OptionalMethodCall {
880                object,
881                method,
882                args,
883                ..
884            } => {
885                self.check_node(object, scope);
886                for arg in args {
887                    self.check_node(arg, scope);
888                }
889                // Definition-site generic checking: if the object's type is a
890                // constrained generic param (where T: Interface), verify the
891                // method exists in the bound interface.
892                if let Some(TypeExpr::Named(type_name)) = self.infer_type(object, scope) {
893                    if scope.is_generic_type_param(&type_name) {
894                        if let Some(iface_name) = scope.get_where_constraint(&type_name) {
895                            if let Some(iface_methods) = scope.get_interface(iface_name) {
896                                let has_method = iface_methods.iter().any(|m| m.name == *method);
897                                if !has_method {
898                                    self.warning_at(
899                                        format!(
900                                            "Method '{}' not found in interface '{}' (constraint on '{}')",
901                                            method, iface_name, type_name
902                                        ),
903                                        span,
904                                    );
905                                }
906                            }
907                        }
908                    }
909                }
910            }
911            Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
912                self.check_node(object, scope);
913            }
914            Node::SubscriptAccess { object, index } => {
915                self.check_node(object, scope);
916                self.check_node(index, scope);
917            }
918            Node::SliceAccess { object, start, end } => {
919                self.check_node(object, scope);
920                if let Some(s) = start {
921                    self.check_node(s, scope);
922                }
923                if let Some(e) = end {
924                    self.check_node(e, scope);
925                }
926            }
927
928            // --- Compound nodes: recurse into children ---
929            Node::Ternary {
930                condition,
931                true_expr,
932                false_expr,
933            } => {
934                self.check_node(condition, scope);
935                let refs = Self::extract_refinements(condition, scope);
936
937                let mut true_scope = scope.child();
938                apply_refinements(&mut true_scope, &refs.truthy);
939                self.check_node(true_expr, &mut true_scope);
940
941                let mut false_scope = scope.child();
942                apply_refinements(&mut false_scope, &refs.falsy);
943                self.check_node(false_expr, &mut false_scope);
944            }
945
946            Node::ThrowStmt { value } => {
947                self.check_node(value, scope);
948            }
949
950            Node::GuardStmt {
951                condition,
952                else_body,
953            } => {
954                self.check_node(condition, scope);
955                let refs = Self::extract_refinements(condition, scope);
956
957                let mut else_scope = scope.child();
958                apply_refinements(&mut else_scope, &refs.falsy);
959                self.check_block(else_body, &mut else_scope);
960
961                // After guard, condition is true — apply truthy refinements
962                // to the OUTER scope (guard's else-body must exit)
963                apply_refinements(scope, &refs.truthy);
964            }
965
966            Node::SpawnExpr { body } => {
967                let mut spawn_scope = scope.child();
968                self.check_block(body, &mut spawn_scope);
969            }
970
971            Node::Parallel {
972                count,
973                variable,
974                body,
975            } => {
976                self.check_node(count, scope);
977                let mut par_scope = scope.child();
978                if let Some(var) = variable {
979                    par_scope.define_var(var, Some(TypeExpr::Named("int".into())));
980                }
981                self.check_block(body, &mut par_scope);
982            }
983
984            Node::ParallelMap {
985                list,
986                variable,
987                body,
988            }
989            | Node::ParallelSettle {
990                list,
991                variable,
992                body,
993            } => {
994                self.check_node(list, scope);
995                let mut par_scope = scope.child();
996                let elem_type = match self.infer_type(list, scope) {
997                    Some(TypeExpr::List(inner)) => Some(*inner),
998                    _ => None,
999                };
1000                par_scope.define_var(variable, elem_type);
1001                self.check_block(body, &mut par_scope);
1002            }
1003
1004            Node::SelectExpr {
1005                cases,
1006                timeout,
1007                default_body,
1008            } => {
1009                for case in cases {
1010                    self.check_node(&case.channel, scope);
1011                    let mut case_scope = scope.child();
1012                    case_scope.define_var(&case.variable, None);
1013                    self.check_block(&case.body, &mut case_scope);
1014                }
1015                if let Some((dur, body)) = timeout {
1016                    self.check_node(dur, scope);
1017                    let mut timeout_scope = scope.child();
1018                    self.check_block(body, &mut timeout_scope);
1019                }
1020                if let Some(body) = default_body {
1021                    let mut default_scope = scope.child();
1022                    self.check_block(body, &mut default_scope);
1023                }
1024            }
1025
1026            Node::DeadlineBlock { duration, body } => {
1027                self.check_node(duration, scope);
1028                let mut block_scope = scope.child();
1029                self.check_block(body, &mut block_scope);
1030            }
1031
1032            Node::MutexBlock { body } => {
1033                let mut block_scope = scope.child();
1034                self.check_block(body, &mut block_scope);
1035            }
1036
1037            Node::Retry { count, body } => {
1038                self.check_node(count, scope);
1039                let mut retry_scope = scope.child();
1040                self.check_block(body, &mut retry_scope);
1041            }
1042
1043            Node::Closure { params, body, .. } => {
1044                let mut closure_scope = scope.child();
1045                for p in params {
1046                    closure_scope.define_var(&p.name, p.type_expr.clone());
1047                }
1048                self.check_block(body, &mut closure_scope);
1049            }
1050
1051            Node::ListLiteral(elements) => {
1052                for elem in elements {
1053                    self.check_node(elem, scope);
1054                }
1055            }
1056
1057            Node::DictLiteral(entries) | Node::AskExpr { fields: entries } => {
1058                for entry in entries {
1059                    self.check_node(&entry.key, scope);
1060                    self.check_node(&entry.value, scope);
1061                }
1062            }
1063
1064            Node::RangeExpr { start, end, .. } => {
1065                self.check_node(start, scope);
1066                self.check_node(end, scope);
1067            }
1068
1069            Node::Spread(inner) => {
1070                self.check_node(inner, scope);
1071            }
1072
1073            Node::Block(stmts) => {
1074                let mut block_scope = scope.child();
1075                self.check_block(stmts, &mut block_scope);
1076            }
1077
1078            Node::YieldExpr { value } => {
1079                if let Some(v) = value {
1080                    self.check_node(v, scope);
1081                }
1082            }
1083
1084            // --- Struct construction: validate fields against declaration ---
1085            Node::StructConstruct {
1086                struct_name,
1087                fields,
1088            } => {
1089                for entry in fields {
1090                    self.check_node(&entry.key, scope);
1091                    self.check_node(&entry.value, scope);
1092                }
1093                if let Some(declared_fields) = scope.get_struct(struct_name).cloned() {
1094                    // Warn on unknown fields
1095                    for entry in fields {
1096                        if let Node::StringLiteral(key) | Node::Identifier(key) = &entry.key.node {
1097                            if !declared_fields.iter().any(|(name, _)| name == key) {
1098                                self.warning_at(
1099                                    format!("Unknown field '{}' in struct '{}'", key, struct_name),
1100                                    entry.key.span,
1101                                );
1102                            }
1103                        }
1104                    }
1105                    // Warn on missing required fields
1106                    let provided: Vec<String> = fields
1107                        .iter()
1108                        .filter_map(|e| match &e.key.node {
1109                            Node::StringLiteral(k) | Node::Identifier(k) => Some(k.clone()),
1110                            _ => None,
1111                        })
1112                        .collect();
1113                    for (name, _) in &declared_fields {
1114                        if !provided.contains(name) {
1115                            self.warning_at(
1116                                format!(
1117                                    "Missing field '{}' in struct '{}' construction",
1118                                    name, struct_name
1119                                ),
1120                                span,
1121                            );
1122                        }
1123                    }
1124                }
1125            }
1126
1127            // --- Enum construction: validate variant exists ---
1128            Node::EnumConstruct {
1129                enum_name,
1130                variant,
1131                args,
1132            } => {
1133                for arg in args {
1134                    self.check_node(arg, scope);
1135                }
1136                if let Some(variants) = scope.get_enum(enum_name) {
1137                    if !variants.contains(variant) {
1138                        self.warning_at(
1139                            format!("Unknown variant '{}' in enum '{}'", variant, enum_name),
1140                            span,
1141                        );
1142                    }
1143                }
1144            }
1145
1146            // --- InterpolatedString: segments are lexer-level, no SNode children ---
1147            Node::InterpolatedString(_) => {}
1148
1149            // --- Terminals: no children to check ---
1150            Node::StringLiteral(_)
1151            | Node::RawStringLiteral(_)
1152            | Node::IntLiteral(_)
1153            | Node::FloatLiteral(_)
1154            | Node::BoolLiteral(_)
1155            | Node::NilLiteral
1156            | Node::Identifier(_)
1157            | Node::DurationLiteral(_)
1158            | Node::BreakStmt
1159            | Node::ContinueStmt
1160            | Node::ReturnStmt { value: None }
1161            | Node::ImportDecl { .. }
1162            | Node::SelectiveImport { .. } => {}
1163
1164            // Declarations already handled above; catch remaining variants
1165            // that have no meaningful type-check behavior.
1166            Node::Pipeline { body, .. } | Node::OverrideDecl { body, .. } => {
1167                let mut decl_scope = scope.child();
1168                self.check_block(body, &mut decl_scope);
1169            }
1170        }
1171    }
1172
1173    fn check_fn_body(
1174        &mut self,
1175        type_params: &[TypeParam],
1176        params: &[TypedParam],
1177        return_type: &Option<TypeExpr>,
1178        body: &[SNode],
1179        where_clauses: &[WhereClause],
1180    ) {
1181        let mut fn_scope = self.scope.child();
1182        // Register generic type parameters so they are treated as compatible
1183        // with any concrete type during type checking.
1184        for tp in type_params {
1185            fn_scope.generic_type_params.insert(tp.name.clone());
1186        }
1187        // Store where-clause constraints for definition-site checking
1188        for wc in where_clauses {
1189            fn_scope
1190                .where_constraints
1191                .insert(wc.type_name.clone(), wc.bound.clone());
1192        }
1193        for param in params {
1194            fn_scope.define_var(&param.name, param.type_expr.clone());
1195            if let Some(default) = &param.default_value {
1196                self.check_node(default, &mut fn_scope);
1197            }
1198        }
1199        // Snapshot scope before main pass (which may mutate it with narrowing)
1200        // so that return-type checking starts from the original parameter types.
1201        let ret_scope_base = if return_type.is_some() {
1202            Some(fn_scope.child())
1203        } else {
1204            None
1205        };
1206
1207        self.check_block(body, &mut fn_scope);
1208
1209        // Check return statements against declared return type
1210        if let Some(ret_type) = return_type {
1211            let mut ret_scope = ret_scope_base.unwrap();
1212            for stmt in body {
1213                self.check_return_type(stmt, ret_type, &mut ret_scope);
1214            }
1215        }
1216    }
1217
1218    fn check_return_type(&mut self, snode: &SNode, expected: &TypeExpr, scope: &mut TypeScope) {
1219        let span = snode.span;
1220        match &snode.node {
1221            Node::ReturnStmt { value: Some(val) } => {
1222                let inferred = self.infer_type(val, scope);
1223                if let Some(actual) = &inferred {
1224                    if !self.types_compatible(expected, actual, scope) {
1225                        self.error_at(
1226                            format!(
1227                                "Return type mismatch: expected {}, got {}",
1228                                format_type(expected),
1229                                format_type(actual)
1230                            ),
1231                            span,
1232                        );
1233                    }
1234                }
1235            }
1236            Node::IfElse {
1237                condition,
1238                then_body,
1239                else_body,
1240            } => {
1241                let refs = Self::extract_refinements(condition, scope);
1242                let mut then_scope = scope.child();
1243                apply_refinements(&mut then_scope, &refs.truthy);
1244                for stmt in then_body {
1245                    self.check_return_type(stmt, expected, &mut then_scope);
1246                }
1247                if let Some(else_body) = else_body {
1248                    let mut else_scope = scope.child();
1249                    apply_refinements(&mut else_scope, &refs.falsy);
1250                    for stmt in else_body {
1251                        self.check_return_type(stmt, expected, &mut else_scope);
1252                    }
1253                    // Post-branch narrowing for return type checking
1254                    if Self::block_definitely_exits(then_body)
1255                        && !Self::block_definitely_exits(else_body)
1256                    {
1257                        apply_refinements(scope, &refs.falsy);
1258                    } else if Self::block_definitely_exits(else_body)
1259                        && !Self::block_definitely_exits(then_body)
1260                    {
1261                        apply_refinements(scope, &refs.truthy);
1262                    }
1263                } else {
1264                    // No else: if then-body always exits, apply falsy after
1265                    if Self::block_definitely_exits(then_body) {
1266                        apply_refinements(scope, &refs.falsy);
1267                    }
1268                }
1269            }
1270            _ => {}
1271        }
1272    }
1273
1274    /// Check if a match expression on an enum's `.variant` property covers all variants.
1275    /// Extract narrowing info from nil-check conditions like `x != nil`.
1276    /// Returns (var_name, narrowed_type) where narrowed_type removes nil from a union.
1277    /// Check if a type satisfies an interface (Go-style implicit satisfaction).
1278    /// A type satisfies an interface if its impl block has all the required methods.
1279    fn satisfies_interface(
1280        &self,
1281        type_name: &str,
1282        interface_name: &str,
1283        scope: &TypeScope,
1284    ) -> bool {
1285        self.interface_mismatch_reason(type_name, interface_name, scope)
1286            .is_none()
1287    }
1288
1289    /// Return a detailed reason why a type does not satisfy an interface, or None
1290    /// if it does satisfy it.  Used for producing actionable warning messages.
1291    fn interface_mismatch_reason(
1292        &self,
1293        type_name: &str,
1294        interface_name: &str,
1295        scope: &TypeScope,
1296    ) -> Option<String> {
1297        let interface_methods = match scope.get_interface(interface_name) {
1298            Some(methods) => methods,
1299            None => return Some(format!("interface '{}' not found", interface_name)),
1300        };
1301        let impl_methods = match scope.get_impl_methods(type_name) {
1302            Some(methods) => methods,
1303            None => {
1304                if interface_methods.is_empty() {
1305                    return None;
1306                }
1307                let names: Vec<_> = interface_methods.iter().map(|m| m.name.as_str()).collect();
1308                return Some(format!("missing method(s): {}", names.join(", ")));
1309            }
1310        };
1311        for iface_method in interface_methods {
1312            let iface_params: Vec<_> = iface_method
1313                .params
1314                .iter()
1315                .filter(|p| p.name != "self")
1316                .collect();
1317            let iface_param_count = iface_params.len();
1318            let matching_impl = impl_methods.iter().find(|im| im.name == iface_method.name);
1319            let impl_method = match matching_impl {
1320                Some(m) => m,
1321                None => {
1322                    return Some(format!("missing method '{}'", iface_method.name));
1323                }
1324            };
1325            if impl_method.param_count != iface_param_count {
1326                return Some(format!(
1327                    "method '{}' has {} parameter(s), expected {}",
1328                    iface_method.name, impl_method.param_count, iface_param_count
1329                ));
1330            }
1331            // Check parameter types where both sides specify them
1332            for (i, iface_param) in iface_params.iter().enumerate() {
1333                if let (Some(expected), Some(actual)) = (
1334                    &iface_param.type_expr,
1335                    impl_method.param_types.get(i).and_then(|t| t.as_ref()),
1336                ) {
1337                    if !self.types_compatible(expected, actual, scope) {
1338                        return Some(format!(
1339                            "method '{}' parameter {} has type '{}', expected '{}'",
1340                            iface_method.name,
1341                            i + 1,
1342                            format_type(actual),
1343                            format_type(expected),
1344                        ));
1345                    }
1346                }
1347            }
1348            // Check return type where both sides specify it
1349            if let (Some(expected_ret), Some(actual_ret)) =
1350                (&iface_method.return_type, &impl_method.return_type)
1351            {
1352                if !self.types_compatible(expected_ret, actual_ret, scope) {
1353                    return Some(format!(
1354                        "method '{}' returns '{}', expected '{}'",
1355                        iface_method.name,
1356                        format_type(actual_ret),
1357                        format_type(expected_ret),
1358                    ));
1359                }
1360            }
1361        }
1362        None
1363    }
1364
1365    /// Recursively extract type parameter bindings from matching param/arg types.
1366    /// E.g., param_type=list<T> + arg_type=list<Dog> → binds T=Dog.
1367    fn extract_type_bindings(
1368        param_type: &TypeExpr,
1369        arg_type: &TypeExpr,
1370        type_params: &std::collections::BTreeSet<String>,
1371        bindings: &mut BTreeMap<String, String>,
1372    ) {
1373        match (param_type, arg_type) {
1374            // Direct type param match: T → concrete
1375            (TypeExpr::Named(param_name), TypeExpr::Named(concrete))
1376                if type_params.contains(param_name) =>
1377            {
1378                bindings
1379                    .entry(param_name.clone())
1380                    .or_insert(concrete.clone());
1381            }
1382            // list<T> + list<Dog>
1383            (TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
1384                Self::extract_type_bindings(p_inner, a_inner, type_params, bindings);
1385            }
1386            // dict<K, V> + dict<string, int>
1387            (TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
1388                Self::extract_type_bindings(pk, ak, type_params, bindings);
1389                Self::extract_type_bindings(pv, av, type_params, bindings);
1390            }
1391            _ => {}
1392        }
1393    }
1394
1395    /// Extract bidirectional type refinements from a condition expression.
1396    fn extract_refinements(condition: &SNode, scope: &TypeScope) -> Refinements {
1397        match &condition.node {
1398            // --- Nil checks and type_of checks ---
1399            Node::BinaryOp { op, left, right } if op == "!=" || op == "==" => {
1400                let nil_ref = Self::extract_nil_refinements(op, left, right, scope);
1401                if !nil_ref.truthy.is_empty() || !nil_ref.falsy.is_empty() {
1402                    return nil_ref;
1403                }
1404                let typeof_ref = Self::extract_typeof_refinements(op, left, right, scope);
1405                if !typeof_ref.truthy.is_empty() || !typeof_ref.falsy.is_empty() {
1406                    return typeof_ref;
1407                }
1408                Refinements::empty()
1409            }
1410
1411            // --- Logical AND: both must be true on truthy path ---
1412            Node::BinaryOp { op, left, right } if op == "&&" => {
1413                let left_ref = Self::extract_refinements(left, scope);
1414                let right_ref = Self::extract_refinements(right, scope);
1415                let mut truthy = left_ref.truthy;
1416                truthy.extend(right_ref.truthy);
1417                Refinements {
1418                    truthy,
1419                    falsy: vec![],
1420                }
1421            }
1422
1423            // --- Logical OR: both must be false on falsy path ---
1424            Node::BinaryOp { op, left, right } if op == "||" => {
1425                let left_ref = Self::extract_refinements(left, scope);
1426                let right_ref = Self::extract_refinements(right, scope);
1427                let mut falsy = left_ref.falsy;
1428                falsy.extend(right_ref.falsy);
1429                Refinements {
1430                    truthy: vec![],
1431                    falsy,
1432                }
1433            }
1434
1435            // --- Negation: swap truthy/falsy ---
1436            Node::UnaryOp { op, operand } if op == "!" => {
1437                Self::extract_refinements(operand, scope).inverted()
1438            }
1439
1440            // --- Truthiness: bare identifier in condition position ---
1441            Node::Identifier(name) => {
1442                if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1443                    if members
1444                        .iter()
1445                        .any(|m| matches!(m, TypeExpr::Named(n) if n == "nil"))
1446                    {
1447                        if let Some(narrowed) = remove_from_union(members, "nil") {
1448                            return Refinements {
1449                                truthy: vec![(name.clone(), Some(narrowed))],
1450                                falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1451                            };
1452                        }
1453                    }
1454                }
1455                Refinements::empty()
1456            }
1457
1458            // --- .has("key") on shapes ---
1459            Node::MethodCall {
1460                object,
1461                method,
1462                args,
1463            } if method == "has" && args.len() == 1 => {
1464                Self::extract_has_refinements(object, args, scope)
1465            }
1466
1467            _ => Refinements::empty(),
1468        }
1469    }
1470
1471    /// Extract nil-check refinements from `x != nil` / `x == nil` patterns.
1472    fn extract_nil_refinements(
1473        op: &str,
1474        left: &SNode,
1475        right: &SNode,
1476        scope: &TypeScope,
1477    ) -> Refinements {
1478        let var_node = if matches!(right.node, Node::NilLiteral) {
1479            left
1480        } else if matches!(left.node, Node::NilLiteral) {
1481            right
1482        } else {
1483            return Refinements::empty();
1484        };
1485
1486        if let Node::Identifier(name) = &var_node.node {
1487            if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1488                if let Some(narrowed) = remove_from_union(members, "nil") {
1489                    let neq_refs = Refinements {
1490                        truthy: vec![(name.clone(), Some(narrowed))],
1491                        falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1492                    };
1493                    return if op == "!=" {
1494                        neq_refs
1495                    } else {
1496                        neq_refs.inverted()
1497                    };
1498                }
1499            }
1500        }
1501        Refinements::empty()
1502    }
1503
1504    /// Extract type_of refinements from `type_of(x) == "typename"` patterns.
1505    fn extract_typeof_refinements(
1506        op: &str,
1507        left: &SNode,
1508        right: &SNode,
1509        scope: &TypeScope,
1510    ) -> Refinements {
1511        let (var_name, type_name) = if let (Some(var), Node::StringLiteral(tn)) =
1512            (extract_type_of_var(left), &right.node)
1513        {
1514            (var, tn.clone())
1515        } else if let (Node::StringLiteral(tn), Some(var)) =
1516            (&left.node, extract_type_of_var(right))
1517        {
1518            (var, tn.clone())
1519        } else {
1520            return Refinements::empty();
1521        };
1522
1523        const KNOWN_TYPES: &[&str] = &[
1524            "int", "string", "float", "bool", "nil", "list", "dict", "closure",
1525        ];
1526        if !KNOWN_TYPES.contains(&type_name.as_str()) {
1527            return Refinements::empty();
1528        }
1529
1530        if let Some(Some(TypeExpr::Union(members))) = scope.get_var(&var_name) {
1531            let narrowed = narrow_to_single(members, &type_name);
1532            let remaining = remove_from_union(members, &type_name);
1533            if narrowed.is_some() || remaining.is_some() {
1534                let eq_refs = Refinements {
1535                    truthy: narrowed
1536                        .map(|n| vec![(var_name.clone(), Some(n))])
1537                        .unwrap_or_default(),
1538                    falsy: remaining
1539                        .map(|r| vec![(var_name.clone(), Some(r))])
1540                        .unwrap_or_default(),
1541                };
1542                return if op == "==" {
1543                    eq_refs
1544                } else {
1545                    eq_refs.inverted()
1546                };
1547            }
1548        }
1549        Refinements::empty()
1550    }
1551
1552    /// Extract .has("key") refinements on shape types.
1553    fn extract_has_refinements(object: &SNode, args: &[SNode], scope: &TypeScope) -> Refinements {
1554        if let Node::Identifier(var_name) = &object.node {
1555            if let Node::StringLiteral(key) = &args[0].node {
1556                if let Some(Some(TypeExpr::Shape(fields))) = scope.get_var(var_name) {
1557                    if fields.iter().any(|f| f.name == *key && f.optional) {
1558                        let narrowed_fields: Vec<ShapeField> = fields
1559                            .iter()
1560                            .map(|f| {
1561                                if f.name == *key {
1562                                    ShapeField {
1563                                        name: f.name.clone(),
1564                                        type_expr: f.type_expr.clone(),
1565                                        optional: false,
1566                                    }
1567                                } else {
1568                                    f.clone()
1569                                }
1570                            })
1571                            .collect();
1572                        return Refinements {
1573                            truthy: vec![(
1574                                var_name.clone(),
1575                                Some(TypeExpr::Shape(narrowed_fields)),
1576                            )],
1577                            falsy: vec![],
1578                        };
1579                    }
1580                }
1581            }
1582        }
1583        Refinements::empty()
1584    }
1585
1586    /// Check whether a block definitely exits (return/throw/break/continue).
1587    fn block_definitely_exits(stmts: &[SNode]) -> bool {
1588        stmts.iter().any(|s| match &s.node {
1589            Node::ReturnStmt { .. }
1590            | Node::ThrowStmt { .. }
1591            | Node::BreakStmt
1592            | Node::ContinueStmt => true,
1593            Node::IfElse {
1594                then_body,
1595                else_body: Some(else_body),
1596                ..
1597            } => Self::block_definitely_exits(then_body) && Self::block_definitely_exits(else_body),
1598            _ => false,
1599        })
1600    }
1601
1602    fn check_match_exhaustiveness(
1603        &mut self,
1604        value: &SNode,
1605        arms: &[MatchArm],
1606        scope: &TypeScope,
1607        span: Span,
1608    ) {
1609        // Detect pattern: match <expr>.variant { "VariantA" -> ... }
1610        let enum_name = match &value.node {
1611            Node::PropertyAccess { object, property } if property == "variant" => {
1612                // Infer the type of the object
1613                match self.infer_type(object, scope) {
1614                    Some(TypeExpr::Named(name)) => {
1615                        if scope.get_enum(&name).is_some() {
1616                            Some(name)
1617                        } else {
1618                            None
1619                        }
1620                    }
1621                    _ => None,
1622                }
1623            }
1624            _ => {
1625                // Direct match on an enum value: match <expr> { ... }
1626                match self.infer_type(value, scope) {
1627                    Some(TypeExpr::Named(name)) if scope.get_enum(&name).is_some() => Some(name),
1628                    _ => None,
1629                }
1630            }
1631        };
1632
1633        let Some(enum_name) = enum_name else {
1634            return;
1635        };
1636        let Some(variants) = scope.get_enum(&enum_name) else {
1637            return;
1638        };
1639
1640        // Collect variant names covered by match arms
1641        let mut covered: Vec<String> = Vec::new();
1642        let mut has_wildcard = false;
1643
1644        for arm in arms {
1645            match &arm.pattern.node {
1646                // String literal pattern (matching on .variant): "VariantA"
1647                Node::StringLiteral(s) => covered.push(s.clone()),
1648                // Identifier pattern acts as a wildcard/catch-all
1649                Node::Identifier(name) if name == "_" || !variants.contains(name) => {
1650                    has_wildcard = true;
1651                }
1652                // Direct enum construct pattern: EnumName.Variant
1653                Node::EnumConstruct { variant, .. } => covered.push(variant.clone()),
1654                // PropertyAccess pattern: EnumName.Variant (no args)
1655                Node::PropertyAccess { property, .. } => covered.push(property.clone()),
1656                _ => {
1657                    // Unknown pattern shape — conservatively treat as wildcard
1658                    has_wildcard = true;
1659                }
1660            }
1661        }
1662
1663        if has_wildcard {
1664            return;
1665        }
1666
1667        let missing: Vec<&String> = variants.iter().filter(|v| !covered.contains(v)).collect();
1668        if !missing.is_empty() {
1669            let missing_str = missing
1670                .iter()
1671                .map(|s| format!("\"{}\"", s))
1672                .collect::<Vec<_>>()
1673                .join(", ");
1674            self.warning_at(
1675                format!(
1676                    "Non-exhaustive match on enum {}: missing variants {}",
1677                    enum_name, missing_str
1678                ),
1679                span,
1680            );
1681        }
1682    }
1683
1684    fn check_call(&mut self, name: &str, args: &[SNode], scope: &mut TypeScope, span: Span) {
1685        // Check against known function signatures
1686        let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
1687        if let Some(sig) = scope.get_fn(name).cloned() {
1688            if !has_spread
1689                && !is_builtin(name)
1690                && !sig.has_rest
1691                && (args.len() < sig.required_params || args.len() > sig.params.len())
1692            {
1693                let expected = if sig.required_params == sig.params.len() {
1694                    format!("{}", sig.params.len())
1695                } else {
1696                    format!("{}-{}", sig.required_params, sig.params.len())
1697                };
1698                self.warning_at(
1699                    format!(
1700                        "Function '{}' expects {} arguments, got {}",
1701                        name,
1702                        expected,
1703                        args.len()
1704                    ),
1705                    span,
1706                );
1707            }
1708            // Build a scope that includes the function's generic type params
1709            // so they are treated as compatible with any concrete type.
1710            let call_scope = if sig.type_param_names.is_empty() {
1711                scope.clone()
1712            } else {
1713                let mut s = scope.child();
1714                for tp_name in &sig.type_param_names {
1715                    s.generic_type_params.insert(tp_name.clone());
1716                }
1717                s
1718            };
1719            for (i, (arg, (param_name, param_type))) in
1720                args.iter().zip(sig.params.iter()).enumerate()
1721            {
1722                if let Some(expected) = param_type {
1723                    let actual = self.infer_type(arg, scope);
1724                    if let Some(actual) = &actual {
1725                        if !self.types_compatible(expected, actual, &call_scope) {
1726                            self.error_at(
1727                                format!(
1728                                    "Argument {} ('{}'): expected {}, got {}",
1729                                    i + 1,
1730                                    param_name,
1731                                    format_type(expected),
1732                                    format_type(actual)
1733                                ),
1734                                arg.span,
1735                            );
1736                        }
1737                    }
1738                }
1739            }
1740            // Enforce where-clause constraints at call site
1741            if !sig.where_clauses.is_empty() {
1742                // Build mapping: type_param → concrete type from inferred args.
1743                // Recursively walks Generic types so list<T> + list<Dog> binds T=Dog.
1744                let mut type_bindings: BTreeMap<String, String> = BTreeMap::new();
1745                let type_param_set: std::collections::BTreeSet<String> =
1746                    sig.type_param_names.iter().cloned().collect();
1747                for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
1748                    if let Some(param_ty) = param_type {
1749                        if let Some(arg_ty) = self.infer_type(arg, scope) {
1750                            Self::extract_type_bindings(
1751                                param_ty,
1752                                &arg_ty,
1753                                &type_param_set,
1754                                &mut type_bindings,
1755                            );
1756                        }
1757                    }
1758                }
1759                for (type_param, bound) in &sig.where_clauses {
1760                    if let Some(concrete_type) = type_bindings.get(type_param) {
1761                        if let Some(reason) =
1762                            self.interface_mismatch_reason(concrete_type, bound, scope)
1763                        {
1764                            self.warning_at(
1765                                format!(
1766                                    "Type '{}' does not satisfy interface '{}': {} \
1767                                     (required by constraint `where {}: {}`)",
1768                                    concrete_type, bound, reason, type_param, bound
1769                                ),
1770                                span,
1771                            );
1772                        }
1773                    }
1774                }
1775            }
1776        }
1777        // Check args recursively
1778        for arg in args {
1779            self.check_node(arg, scope);
1780        }
1781    }
1782
1783    /// Infer the type of an expression.
1784    fn infer_type(&self, snode: &SNode, scope: &TypeScope) -> InferredType {
1785        match &snode.node {
1786            Node::IntLiteral(_) => Some(TypeExpr::Named("int".into())),
1787            Node::FloatLiteral(_) => Some(TypeExpr::Named("float".into())),
1788            Node::StringLiteral(_) | Node::InterpolatedString(_) => {
1789                Some(TypeExpr::Named("string".into()))
1790            }
1791            Node::BoolLiteral(_) => Some(TypeExpr::Named("bool".into())),
1792            Node::NilLiteral => Some(TypeExpr::Named("nil".into())),
1793            Node::ListLiteral(_) => Some(TypeExpr::Named("list".into())),
1794            Node::DictLiteral(entries) => {
1795                // Infer shape type when all keys are string literals
1796                let mut fields = Vec::new();
1797                let mut all_string_keys = true;
1798                for entry in entries {
1799                    if let Node::StringLiteral(key) = &entry.key.node {
1800                        let val_type = self
1801                            .infer_type(&entry.value, scope)
1802                            .unwrap_or(TypeExpr::Named("nil".into()));
1803                        fields.push(ShapeField {
1804                            name: key.clone(),
1805                            type_expr: val_type,
1806                            optional: false,
1807                        });
1808                    } else {
1809                        all_string_keys = false;
1810                        break;
1811                    }
1812                }
1813                if all_string_keys && !fields.is_empty() {
1814                    Some(TypeExpr::Shape(fields))
1815                } else {
1816                    Some(TypeExpr::Named("dict".into()))
1817                }
1818            }
1819            Node::Closure { params, body, .. } => {
1820                // If all params are typed and we can infer a return type, produce FnType
1821                let all_typed = params.iter().all(|p| p.type_expr.is_some());
1822                if all_typed && !params.is_empty() {
1823                    let param_types: Vec<TypeExpr> =
1824                        params.iter().filter_map(|p| p.type_expr.clone()).collect();
1825                    // Try to infer return type from last expression in body
1826                    let ret = body.last().and_then(|last| self.infer_type(last, scope));
1827                    if let Some(ret_type) = ret {
1828                        return Some(TypeExpr::FnType {
1829                            params: param_types,
1830                            return_type: Box::new(ret_type),
1831                        });
1832                    }
1833                }
1834                Some(TypeExpr::Named("closure".into()))
1835            }
1836
1837            Node::Identifier(name) => scope.get_var(name).cloned().flatten(),
1838
1839            Node::FunctionCall { name, .. } => {
1840                // Struct constructor calls return the struct type
1841                if scope.get_struct(name).is_some() {
1842                    return Some(TypeExpr::Named(name.clone()));
1843                }
1844                // Check user-defined function return types
1845                if let Some(sig) = scope.get_fn(name) {
1846                    return sig.return_type.clone();
1847                }
1848                // Check builtin return types
1849                builtin_return_type(name)
1850            }
1851
1852            Node::BinaryOp { op, left, right } => {
1853                let lt = self.infer_type(left, scope);
1854                let rt = self.infer_type(right, scope);
1855                infer_binary_op_type(op, &lt, &rt)
1856            }
1857
1858            Node::UnaryOp { op, operand } => {
1859                let t = self.infer_type(operand, scope);
1860                match op.as_str() {
1861                    "!" => Some(TypeExpr::Named("bool".into())),
1862                    "-" => t, // negation preserves type
1863                    _ => None,
1864                }
1865            }
1866
1867            Node::Ternary {
1868                condition,
1869                true_expr,
1870                false_expr,
1871            } => {
1872                let refs = Self::extract_refinements(condition, scope);
1873
1874                let mut true_scope = scope.child();
1875                apply_refinements(&mut true_scope, &refs.truthy);
1876                let tt = self.infer_type(true_expr, &true_scope);
1877
1878                let mut false_scope = scope.child();
1879                apply_refinements(&mut false_scope, &refs.falsy);
1880                let ft = self.infer_type(false_expr, &false_scope);
1881
1882                match (&tt, &ft) {
1883                    (Some(a), Some(b)) if a == b => tt,
1884                    (Some(a), Some(b)) => Some(TypeExpr::Union(vec![a.clone(), b.clone()])),
1885                    (Some(_), None) => tt,
1886                    (None, Some(_)) => ft,
1887                    (None, None) => None,
1888                }
1889            }
1890
1891            Node::EnumConstruct { enum_name, .. } => Some(TypeExpr::Named(enum_name.clone())),
1892
1893            Node::PropertyAccess { object, property } => {
1894                // EnumName.Variant → infer as the enum type
1895                if let Node::Identifier(name) = &object.node {
1896                    if scope.get_enum(name).is_some() {
1897                        return Some(TypeExpr::Named(name.clone()));
1898                    }
1899                }
1900                // .variant on an enum value → string
1901                if property == "variant" {
1902                    let obj_type = self.infer_type(object, scope);
1903                    if let Some(TypeExpr::Named(name)) = &obj_type {
1904                        if scope.get_enum(name).is_some() {
1905                            return Some(TypeExpr::Named("string".into()));
1906                        }
1907                    }
1908                }
1909                // Shape field access: obj.field → field type
1910                let obj_type = self.infer_type(object, scope);
1911                if let Some(TypeExpr::Shape(fields)) = &obj_type {
1912                    if let Some(field) = fields.iter().find(|f| f.name == *property) {
1913                        return Some(field.type_expr.clone());
1914                    }
1915                }
1916                None
1917            }
1918
1919            Node::SubscriptAccess { object, index } => {
1920                let obj_type = self.infer_type(object, scope);
1921                match &obj_type {
1922                    Some(TypeExpr::List(inner)) => Some(*inner.clone()),
1923                    Some(TypeExpr::DictType(_, v)) => Some(*v.clone()),
1924                    Some(TypeExpr::Shape(fields)) => {
1925                        // If index is a string literal, look up the field type
1926                        if let Node::StringLiteral(key) = &index.node {
1927                            fields
1928                                .iter()
1929                                .find(|f| &f.name == key)
1930                                .map(|f| f.type_expr.clone())
1931                        } else {
1932                            None
1933                        }
1934                    }
1935                    Some(TypeExpr::Named(n)) if n == "list" => None,
1936                    Some(TypeExpr::Named(n)) if n == "dict" => None,
1937                    Some(TypeExpr::Named(n)) if n == "string" => {
1938                        Some(TypeExpr::Named("string".into()))
1939                    }
1940                    _ => None,
1941                }
1942            }
1943            Node::SliceAccess { object, .. } => {
1944                // Slicing a list returns the same list type; slicing a string returns string
1945                let obj_type = self.infer_type(object, scope);
1946                match &obj_type {
1947                    Some(TypeExpr::List(_)) => obj_type,
1948                    Some(TypeExpr::Named(n)) if n == "list" => obj_type,
1949                    Some(TypeExpr::Named(n)) if n == "string" => {
1950                        Some(TypeExpr::Named("string".into()))
1951                    }
1952                    _ => None,
1953                }
1954            }
1955            Node::MethodCall { object, method, .. }
1956            | Node::OptionalMethodCall { object, method, .. } => {
1957                let obj_type = self.infer_type(object, scope);
1958                let is_dict = matches!(&obj_type, Some(TypeExpr::Named(n)) if n == "dict")
1959                    || matches!(&obj_type, Some(TypeExpr::DictType(..)))
1960                    || matches!(&obj_type, Some(TypeExpr::Shape(_)));
1961                match method.as_str() {
1962                    // Shared: bool-returning methods
1963                    "contains" | "starts_with" | "ends_with" | "empty" | "has" | "any" | "all" => {
1964                        Some(TypeExpr::Named("bool".into()))
1965                    }
1966                    // Shared: int-returning methods
1967                    "count" | "index_of" => Some(TypeExpr::Named("int".into())),
1968                    // String methods
1969                    "trim" | "lowercase" | "uppercase" | "reverse" | "replace" | "substring"
1970                    | "pad_left" | "pad_right" | "repeat" | "join" => {
1971                        Some(TypeExpr::Named("string".into()))
1972                    }
1973                    "split" | "chars" => Some(TypeExpr::Named("list".into())),
1974                    // filter returns dict for dicts, list for lists
1975                    "filter" => {
1976                        if is_dict {
1977                            Some(TypeExpr::Named("dict".into()))
1978                        } else {
1979                            Some(TypeExpr::Named("list".into()))
1980                        }
1981                    }
1982                    // List methods
1983                    "map" | "flat_map" | "sort" => Some(TypeExpr::Named("list".into())),
1984                    "reduce" | "find" | "first" | "last" => None,
1985                    // Dict methods
1986                    "keys" | "values" | "entries" => Some(TypeExpr::Named("list".into())),
1987                    "merge" | "map_values" | "rekey" | "map_keys" => {
1988                        // Rekey/map_keys transform keys; resulting dict still keys-by-string.
1989                        // Preserve the value-type parameter when known so downstream code can
1990                        // still rely on dict<string, V> typing after a key-rename.
1991                        if let Some(TypeExpr::DictType(_, v)) = &obj_type {
1992                            Some(TypeExpr::DictType(
1993                                Box::new(TypeExpr::Named("string".into())),
1994                                v.clone(),
1995                            ))
1996                        } else {
1997                            Some(TypeExpr::Named("dict".into()))
1998                        }
1999                    }
2000                    // Conversions
2001                    "to_string" => Some(TypeExpr::Named("string".into())),
2002                    "to_int" => Some(TypeExpr::Named("int".into())),
2003                    "to_float" => Some(TypeExpr::Named("float".into())),
2004                    _ => None,
2005                }
2006            }
2007
2008            // TryOperator on Result<T, E> produces T
2009            Node::TryOperator { operand } => {
2010                match self.infer_type(operand, scope) {
2011                    Some(TypeExpr::Named(name)) if name == "Result" => None, // unknown inner type
2012                    _ => None,
2013                }
2014            }
2015
2016            _ => None,
2017        }
2018    }
2019
2020    /// Check if two types are compatible (actual can be assigned to expected).
2021    fn types_compatible(&self, expected: &TypeExpr, actual: &TypeExpr, scope: &TypeScope) -> bool {
2022        // Generic type parameters match anything.
2023        if let TypeExpr::Named(name) = expected {
2024            if scope.is_generic_type_param(name) {
2025                return true;
2026            }
2027        }
2028        if let TypeExpr::Named(name) = actual {
2029            if scope.is_generic_type_param(name) {
2030                return true;
2031            }
2032        }
2033        let expected = self.resolve_alias(expected, scope);
2034        let actual = self.resolve_alias(actual, scope);
2035
2036        // Interface satisfaction: if expected is an interface name, check if actual type
2037        // has all required methods (Go-style implicit satisfaction).
2038        if let TypeExpr::Named(iface_name) = &expected {
2039            if scope.get_interface(iface_name).is_some() {
2040                if let TypeExpr::Named(type_name) = &actual {
2041                    return self.satisfies_interface(type_name, iface_name, scope);
2042                }
2043                return false;
2044            }
2045        }
2046
2047        match (&expected, &actual) {
2048            (TypeExpr::Named(a), TypeExpr::Named(b)) => a == b || (a == "float" && b == "int"),
2049            // Union-to-Union: every member of actual must be compatible with
2050            // at least one member of expected.
2051            (TypeExpr::Union(exp_members), TypeExpr::Union(act_members)) => {
2052                act_members.iter().all(|am| {
2053                    exp_members
2054                        .iter()
2055                        .any(|em| self.types_compatible(em, am, scope))
2056                })
2057            }
2058            (TypeExpr::Union(members), actual_type) => members
2059                .iter()
2060                .any(|m| self.types_compatible(m, actual_type, scope)),
2061            (expected_type, TypeExpr::Union(members)) => members
2062                .iter()
2063                .all(|m| self.types_compatible(expected_type, m, scope)),
2064            (TypeExpr::Shape(_), TypeExpr::Named(n)) if n == "dict" => true,
2065            (TypeExpr::Named(n), TypeExpr::Shape(_)) if n == "dict" => true,
2066            (TypeExpr::Shape(ef), TypeExpr::Shape(af)) => ef.iter().all(|expected_field| {
2067                if expected_field.optional {
2068                    return true;
2069                }
2070                af.iter().any(|actual_field| {
2071                    actual_field.name == expected_field.name
2072                        && self.types_compatible(
2073                            &expected_field.type_expr,
2074                            &actual_field.type_expr,
2075                            scope,
2076                        )
2077                })
2078            }),
2079            // dict<K, V> expected, Shape actual → all field values must match V
2080            (TypeExpr::DictType(ek, ev), TypeExpr::Shape(af)) => {
2081                let keys_ok = matches!(ek.as_ref(), TypeExpr::Named(n) if n == "string");
2082                keys_ok
2083                    && af
2084                        .iter()
2085                        .all(|f| self.types_compatible(ev, &f.type_expr, scope))
2086            }
2087            // Shape expected, dict<K, V> actual → gradual: allow since dict may have the fields
2088            (TypeExpr::Shape(_), TypeExpr::DictType(_, _)) => true,
2089            (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
2090                self.types_compatible(expected_inner, actual_inner, scope)
2091            }
2092            (TypeExpr::Named(n), TypeExpr::List(_)) if n == "list" => true,
2093            (TypeExpr::List(_), TypeExpr::Named(n)) if n == "list" => true,
2094            (TypeExpr::DictType(ek, ev), TypeExpr::DictType(ak, av)) => {
2095                self.types_compatible(ek, ak, scope) && self.types_compatible(ev, av, scope)
2096            }
2097            (TypeExpr::Named(n), TypeExpr::DictType(_, _)) if n == "dict" => true,
2098            (TypeExpr::DictType(_, _), TypeExpr::Named(n)) if n == "dict" => true,
2099            // FnType compatibility: params match positionally and return types match
2100            (
2101                TypeExpr::FnType {
2102                    params: ep,
2103                    return_type: er,
2104                },
2105                TypeExpr::FnType {
2106                    params: ap,
2107                    return_type: ar,
2108                },
2109            ) => {
2110                ep.len() == ap.len()
2111                    && ep
2112                        .iter()
2113                        .zip(ap.iter())
2114                        .all(|(e, a)| self.types_compatible(e, a, scope))
2115                    && self.types_compatible(er, ar, scope)
2116            }
2117            // FnType is compatible with Named("closure") for backward compat
2118            (TypeExpr::FnType { .. }, TypeExpr::Named(n)) if n == "closure" => true,
2119            (TypeExpr::Named(n), TypeExpr::FnType { .. }) if n == "closure" => true,
2120            _ => false,
2121        }
2122    }
2123
2124    fn resolve_alias<'a>(&self, ty: &'a TypeExpr, scope: &'a TypeScope) -> TypeExpr {
2125        if let TypeExpr::Named(name) = ty {
2126            if let Some(resolved) = scope.resolve_type(name) {
2127                return resolved.clone();
2128            }
2129        }
2130        ty.clone()
2131    }
2132
2133    fn error_at(&mut self, message: String, span: Span) {
2134        self.diagnostics.push(TypeDiagnostic {
2135            message,
2136            severity: DiagnosticSeverity::Error,
2137            span: Some(span),
2138            help: None,
2139        });
2140    }
2141
2142    #[allow(dead_code)]
2143    fn error_at_with_help(&mut self, message: String, span: Span, help: String) {
2144        self.diagnostics.push(TypeDiagnostic {
2145            message,
2146            severity: DiagnosticSeverity::Error,
2147            span: Some(span),
2148            help: Some(help),
2149        });
2150    }
2151
2152    fn warning_at(&mut self, message: String, span: Span) {
2153        self.diagnostics.push(TypeDiagnostic {
2154            message,
2155            severity: DiagnosticSeverity::Warning,
2156            span: Some(span),
2157            help: None,
2158        });
2159    }
2160
2161    #[allow(dead_code)]
2162    fn warning_at_with_help(&mut self, message: String, span: Span, help: String) {
2163        self.diagnostics.push(TypeDiagnostic {
2164            message,
2165            severity: DiagnosticSeverity::Warning,
2166            span: Some(span),
2167            help: Some(help),
2168        });
2169    }
2170}
2171
2172impl Default for TypeChecker {
2173    fn default() -> Self {
2174        Self::new()
2175    }
2176}
2177
2178/// Infer the result type of a binary operation.
2179fn infer_binary_op_type(op: &str, left: &InferredType, right: &InferredType) -> InferredType {
2180    match op {
2181        "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
2182            Some(TypeExpr::Named("bool".into()))
2183        }
2184        "+" => match (left, right) {
2185            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2186                match (l.as_str(), r.as_str()) {
2187                    ("int", "int") => Some(TypeExpr::Named("int".into())),
2188                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2189                    ("string", _) => Some(TypeExpr::Named("string".into())),
2190                    ("list", "list") => Some(TypeExpr::Named("list".into())),
2191                    ("dict", "dict") => Some(TypeExpr::Named("dict".into())),
2192                    _ => Some(TypeExpr::Named("string".into())),
2193                }
2194            }
2195            _ => None,
2196        },
2197        "-" | "/" | "%" => match (left, right) {
2198            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2199                match (l.as_str(), r.as_str()) {
2200                    ("int", "int") => Some(TypeExpr::Named("int".into())),
2201                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2202                    _ => None,
2203                }
2204            }
2205            _ => None,
2206        },
2207        "*" => match (left, right) {
2208            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2209                match (l.as_str(), r.as_str()) {
2210                    ("string", "int") | ("int", "string") => Some(TypeExpr::Named("string".into())),
2211                    ("int", "int") => Some(TypeExpr::Named("int".into())),
2212                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2213                    _ => None,
2214                }
2215            }
2216            _ => None,
2217        },
2218        "??" => match (left, right) {
2219            (Some(TypeExpr::Union(members)), _) => {
2220                let non_nil: Vec<_> = members
2221                    .iter()
2222                    .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
2223                    .cloned()
2224                    .collect();
2225                if non_nil.len() == 1 {
2226                    Some(non_nil[0].clone())
2227                } else if non_nil.is_empty() {
2228                    right.clone()
2229                } else {
2230                    Some(TypeExpr::Union(non_nil))
2231                }
2232            }
2233            _ => right.clone(),
2234        },
2235        "|>" => None,
2236        _ => None,
2237    }
2238}
2239
2240/// Format a type expression for display in error messages.
2241/// Produce a detail string describing why a Shape type is incompatible with
2242/// another Shape type — e.g. "missing field 'age' (int)" or "field 'name'
2243/// has type int, expected string".  Returns `None` if both types are not shapes.
2244pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
2245    if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
2246        let mut details = Vec::new();
2247        for field in ef {
2248            if field.optional {
2249                continue;
2250            }
2251            match af.iter().find(|f| f.name == field.name) {
2252                None => details.push(format!(
2253                    "missing field '{}' ({})",
2254                    field.name,
2255                    format_type(&field.type_expr)
2256                )),
2257                Some(actual_field) => {
2258                    let e_str = format_type(&field.type_expr);
2259                    let a_str = format_type(&actual_field.type_expr);
2260                    if e_str != a_str {
2261                        details.push(format!(
2262                            "field '{}' has type {}, expected {}",
2263                            field.name, a_str, e_str
2264                        ));
2265                    }
2266                }
2267            }
2268        }
2269        if details.is_empty() {
2270            None
2271        } else {
2272            Some(details.join("; "))
2273        }
2274    } else {
2275        None
2276    }
2277}
2278
2279pub fn format_type(ty: &TypeExpr) -> String {
2280    match ty {
2281        TypeExpr::Named(n) => n.clone(),
2282        TypeExpr::Union(types) => types
2283            .iter()
2284            .map(format_type)
2285            .collect::<Vec<_>>()
2286            .join(" | "),
2287        TypeExpr::Shape(fields) => {
2288            let inner: Vec<String> = fields
2289                .iter()
2290                .map(|f| {
2291                    let opt = if f.optional { "?" } else { "" };
2292                    format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
2293                })
2294                .collect();
2295            format!("{{{}}}", inner.join(", "))
2296        }
2297        TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
2298        TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
2299        TypeExpr::FnType {
2300            params,
2301            return_type,
2302        } => {
2303            let params_str = params
2304                .iter()
2305                .map(format_type)
2306                .collect::<Vec<_>>()
2307                .join(", ");
2308            format!("fn({}) -> {}", params_str, format_type(return_type))
2309        }
2310    }
2311}
2312
2313/// Remove a named type from a union, collapsing single-element unions.
2314fn remove_from_union(members: &[TypeExpr], to_remove: &str) -> InferredType {
2315    let remaining: Vec<TypeExpr> = members
2316        .iter()
2317        .filter(|m| !matches!(m, TypeExpr::Named(n) if n == to_remove))
2318        .cloned()
2319        .collect();
2320    match remaining.len() {
2321        0 => None,
2322        1 => Some(remaining.into_iter().next().unwrap()),
2323        _ => Some(TypeExpr::Union(remaining)),
2324    }
2325}
2326
2327/// Narrow a union to just one named type, if that type is a member.
2328fn narrow_to_single(members: &[TypeExpr], target: &str) -> InferredType {
2329    if members
2330        .iter()
2331        .any(|m| matches!(m, TypeExpr::Named(n) if n == target))
2332    {
2333        Some(TypeExpr::Named(target.to_string()))
2334    } else {
2335        None
2336    }
2337}
2338
2339/// Extract the variable name from a `type_of(x)` call.
2340fn extract_type_of_var(node: &SNode) -> Option<String> {
2341    if let Node::FunctionCall { name, args } = &node.node {
2342        if name == "type_of" && args.len() == 1 {
2343            if let Node::Identifier(var) = &args[0].node {
2344                return Some(var.clone());
2345            }
2346        }
2347    }
2348    None
2349}
2350
2351/// Apply a list of refinements to a scope, tracking pre-narrowing types.
2352fn apply_refinements(scope: &mut TypeScope, refinements: &[(String, InferredType)]) {
2353    for (var_name, narrowed_type) in refinements {
2354        // Save the pre-narrowing type so we can restore it on reassignment
2355        if !scope.narrowed_vars.contains_key(var_name) {
2356            if let Some(original) = scope.get_var(var_name).cloned() {
2357                scope.narrowed_vars.insert(var_name.clone(), original);
2358            }
2359        }
2360        scope.define_var(var_name, narrowed_type.clone());
2361    }
2362}
2363
2364#[cfg(test)]
2365mod tests {
2366    use super::*;
2367    use crate::Parser;
2368    use harn_lexer::Lexer;
2369
2370    fn check_source(source: &str) -> Vec<TypeDiagnostic> {
2371        let mut lexer = Lexer::new(source);
2372        let tokens = lexer.tokenize().unwrap();
2373        let mut parser = Parser::new(tokens);
2374        let program = parser.parse().unwrap();
2375        TypeChecker::new().check(&program)
2376    }
2377
2378    fn errors(source: &str) -> Vec<String> {
2379        check_source(source)
2380            .into_iter()
2381            .filter(|d| d.severity == DiagnosticSeverity::Error)
2382            .map(|d| d.message)
2383            .collect()
2384    }
2385
2386    #[test]
2387    fn test_no_errors_for_untyped_code() {
2388        let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
2389        assert!(errs.is_empty());
2390    }
2391
2392    #[test]
2393    fn test_correct_typed_let() {
2394        let errs = errors("pipeline t(task) { let x: int = 42 }");
2395        assert!(errs.is_empty());
2396    }
2397
2398    #[test]
2399    fn test_type_mismatch_let() {
2400        let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
2401        assert_eq!(errs.len(), 1);
2402        assert!(errs[0].contains("Type mismatch"));
2403        assert!(errs[0].contains("int"));
2404        assert!(errs[0].contains("string"));
2405    }
2406
2407    #[test]
2408    fn test_correct_typed_fn() {
2409        let errs = errors(
2410            "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
2411        );
2412        assert!(errs.is_empty());
2413    }
2414
2415    #[test]
2416    fn test_fn_arg_type_mismatch() {
2417        let errs = errors(
2418            r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
2419add("hello", 2) }"#,
2420        );
2421        assert_eq!(errs.len(), 1);
2422        assert!(errs[0].contains("Argument 1"));
2423        assert!(errs[0].contains("expected int"));
2424    }
2425
2426    #[test]
2427    fn test_return_type_mismatch() {
2428        let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
2429        assert_eq!(errs.len(), 1);
2430        assert!(errs[0].contains("Return type mismatch"));
2431    }
2432
2433    #[test]
2434    fn test_union_type_compatible() {
2435        let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
2436        assert!(errs.is_empty());
2437    }
2438
2439    #[test]
2440    fn test_union_type_mismatch() {
2441        let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
2442        assert_eq!(errs.len(), 1);
2443        assert!(errs[0].contains("Type mismatch"));
2444    }
2445
2446    #[test]
2447    fn test_type_inference_propagation() {
2448        let errs = errors(
2449            r#"pipeline t(task) {
2450  fn add(a: int, b: int) -> int { return a + b }
2451  let result: string = add(1, 2)
2452}"#,
2453        );
2454        assert_eq!(errs.len(), 1);
2455        assert!(errs[0].contains("Type mismatch"));
2456        assert!(errs[0].contains("string"));
2457        assert!(errs[0].contains("int"));
2458    }
2459
2460    #[test]
2461    fn test_builtin_return_type_inference() {
2462        let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
2463        assert_eq!(errs.len(), 1);
2464        assert!(errs[0].contains("string"));
2465        assert!(errs[0].contains("int"));
2466    }
2467
2468    #[test]
2469    fn test_workflow_and_transcript_builtins_are_known() {
2470        let errs = errors(
2471            r#"pipeline t(task) {
2472  let flow = workflow_graph({name: "demo", entry: "act", nodes: {act: {kind: "stage"}}})
2473  let report: dict = workflow_policy_report(flow, {tools: tool_registry(), capabilities: {workspace: ["read_text"]}})
2474  let run: dict = workflow_execute("task", flow, [], {})
2475  let tree: dict = load_run_tree("run.json")
2476  let fixture: dict = run_record_fixture(run?.run)
2477  let suite: dict = run_record_eval_suite([{run: run?.run, fixture: fixture}])
2478  let diff: dict = run_record_diff(run?.run, run?.run)
2479  let manifest: dict = eval_suite_manifest({cases: [{run_path: "run.json"}]})
2480  let suite_report: dict = eval_suite_run(manifest)
2481  let wf: dict = artifact_workspace_file("src/main.rs", "fn main() {}", {source: "host"})
2482  let snap: dict = artifact_workspace_snapshot(["src/main.rs"], "snapshot")
2483  let selection: dict = artifact_editor_selection("src/main.rs", "main")
2484  let verify: dict = artifact_verification_result("verify", "ok")
2485  let test_result: dict = artifact_test_result("tests", "pass")
2486  let cmd: dict = artifact_command_result("cargo test", {status: 0})
2487  let patch: dict = artifact_diff("src/main.rs", "old", "new")
2488  let git: dict = artifact_git_diff("diff --git a b")
2489  let review: dict = artifact_diff_review(patch, "review me")
2490  let decision: dict = artifact_review_decision(review, "accepted")
2491  let proposal: dict = artifact_patch_proposal(review, "*** Begin Patch")
2492  let bundle: dict = artifact_verification_bundle("checks", [{name: "fmt", ok: true}])
2493  let apply: dict = artifact_apply_intent(review, "apply")
2494  let transcript = transcript_reset({metadata: {source: "test"}})
2495  let visible: string = transcript_render_visible(transcript_archive(transcript))
2496  let events: list = transcript_events(transcript)
2497  let context: string = artifact_context([], {max_artifacts: 1})
2498  println(report)
2499  println(run)
2500  println(tree)
2501  println(fixture)
2502  println(suite)
2503  println(diff)
2504  println(manifest)
2505  println(suite_report)
2506  println(wf)
2507  println(snap)
2508  println(selection)
2509  println(verify)
2510  println(test_result)
2511  println(cmd)
2512  println(patch)
2513  println(git)
2514  println(review)
2515  println(decision)
2516  println(proposal)
2517  println(bundle)
2518  println(apply)
2519  println(visible)
2520  println(events)
2521  println(context)
2522}"#,
2523        );
2524        assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
2525    }
2526
2527    #[test]
2528    fn test_binary_op_type_inference() {
2529        let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
2530        assert_eq!(errs.len(), 1);
2531    }
2532
2533    #[test]
2534    fn test_comparison_returns_bool() {
2535        let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
2536        assert!(errs.is_empty());
2537    }
2538
2539    #[test]
2540    fn test_int_float_promotion() {
2541        let errs = errors("pipeline t(task) { let x: float = 42 }");
2542        assert!(errs.is_empty());
2543    }
2544
2545    #[test]
2546    fn test_untyped_code_no_errors() {
2547        let errs = errors(
2548            r#"pipeline t(task) {
2549  fn process(data) {
2550    let result = data + " processed"
2551    return result
2552  }
2553  log(process("hello"))
2554}"#,
2555        );
2556        assert!(errs.is_empty());
2557    }
2558
2559    #[test]
2560    fn test_type_alias() {
2561        let errs = errors(
2562            r#"pipeline t(task) {
2563  type Name = string
2564  let x: Name = "hello"
2565}"#,
2566        );
2567        assert!(errs.is_empty());
2568    }
2569
2570    #[test]
2571    fn test_type_alias_mismatch() {
2572        let errs = errors(
2573            r#"pipeline t(task) {
2574  type Name = string
2575  let x: Name = 42
2576}"#,
2577        );
2578        assert_eq!(errs.len(), 1);
2579    }
2580
2581    #[test]
2582    fn test_assignment_type_check() {
2583        let errs = errors(
2584            r#"pipeline t(task) {
2585  var x: int = 0
2586  x = "hello"
2587}"#,
2588        );
2589        assert_eq!(errs.len(), 1);
2590        assert!(errs[0].contains("cannot assign string"));
2591    }
2592
2593    #[test]
2594    fn test_covariance_int_to_float_in_fn() {
2595        let errs = errors(
2596            "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
2597        );
2598        assert!(errs.is_empty());
2599    }
2600
2601    #[test]
2602    fn test_covariance_return_type() {
2603        let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
2604        assert!(errs.is_empty());
2605    }
2606
2607    #[test]
2608    fn test_no_contravariance_float_to_int() {
2609        let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
2610        assert_eq!(errs.len(), 1);
2611    }
2612
2613    // --- Exhaustiveness checking tests ---
2614
2615    fn warnings(source: &str) -> Vec<String> {
2616        check_source(source)
2617            .into_iter()
2618            .filter(|d| d.severity == DiagnosticSeverity::Warning)
2619            .map(|d| d.message)
2620            .collect()
2621    }
2622
2623    #[test]
2624    fn test_exhaustive_match_no_warning() {
2625        let warns = warnings(
2626            r#"pipeline t(task) {
2627  enum Color { Red, Green, Blue }
2628  let c = Color.Red
2629  match c.variant {
2630    "Red" -> { log("r") }
2631    "Green" -> { log("g") }
2632    "Blue" -> { log("b") }
2633  }
2634}"#,
2635        );
2636        let exhaustive_warns: Vec<_> = warns
2637            .iter()
2638            .filter(|w| w.contains("Non-exhaustive"))
2639            .collect();
2640        assert!(exhaustive_warns.is_empty());
2641    }
2642
2643    #[test]
2644    fn test_non_exhaustive_match_warning() {
2645        let warns = warnings(
2646            r#"pipeline t(task) {
2647  enum Color { Red, Green, Blue }
2648  let c = Color.Red
2649  match c.variant {
2650    "Red" -> { log("r") }
2651    "Green" -> { log("g") }
2652  }
2653}"#,
2654        );
2655        let exhaustive_warns: Vec<_> = warns
2656            .iter()
2657            .filter(|w| w.contains("Non-exhaustive"))
2658            .collect();
2659        assert_eq!(exhaustive_warns.len(), 1);
2660        assert!(exhaustive_warns[0].contains("Blue"));
2661    }
2662
2663    #[test]
2664    fn test_non_exhaustive_multiple_missing() {
2665        let warns = warnings(
2666            r#"pipeline t(task) {
2667  enum Status { Active, Inactive, Pending }
2668  let s = Status.Active
2669  match s.variant {
2670    "Active" -> { log("a") }
2671  }
2672}"#,
2673        );
2674        let exhaustive_warns: Vec<_> = warns
2675            .iter()
2676            .filter(|w| w.contains("Non-exhaustive"))
2677            .collect();
2678        assert_eq!(exhaustive_warns.len(), 1);
2679        assert!(exhaustive_warns[0].contains("Inactive"));
2680        assert!(exhaustive_warns[0].contains("Pending"));
2681    }
2682
2683    #[test]
2684    fn test_enum_construct_type_inference() {
2685        let errs = errors(
2686            r#"pipeline t(task) {
2687  enum Color { Red, Green, Blue }
2688  let c: Color = Color.Red
2689}"#,
2690        );
2691        assert!(errs.is_empty());
2692    }
2693
2694    // --- Type narrowing tests ---
2695
2696    #[test]
2697    fn test_nil_coalescing_strips_nil() {
2698        // After ??, nil should be stripped from the type
2699        let errs = errors(
2700            r#"pipeline t(task) {
2701  let x: string | nil = nil
2702  let y: string = x ?? "default"
2703}"#,
2704        );
2705        assert!(errs.is_empty());
2706    }
2707
2708    #[test]
2709    fn test_shape_mismatch_detail_missing_field() {
2710        let errs = errors(
2711            r#"pipeline t(task) {
2712  let x: {name: string, age: int} = {name: "hello"}
2713}"#,
2714        );
2715        assert_eq!(errs.len(), 1);
2716        assert!(
2717            errs[0].contains("missing field 'age'"),
2718            "expected detail about missing field, got: {}",
2719            errs[0]
2720        );
2721    }
2722
2723    #[test]
2724    fn test_shape_mismatch_detail_wrong_type() {
2725        let errs = errors(
2726            r#"pipeline t(task) {
2727  let x: {name: string, age: int} = {name: 42, age: 10}
2728}"#,
2729        );
2730        assert_eq!(errs.len(), 1);
2731        assert!(
2732            errs[0].contains("field 'name' has type int, expected string"),
2733            "expected detail about wrong type, got: {}",
2734            errs[0]
2735        );
2736    }
2737
2738    // --- Match pattern type validation tests ---
2739
2740    #[test]
2741    fn test_match_pattern_string_against_int() {
2742        let warns = warnings(
2743            r#"pipeline t(task) {
2744  let x: int = 42
2745  match x {
2746    "hello" -> { log("bad") }
2747    42 -> { log("ok") }
2748  }
2749}"#,
2750        );
2751        let pattern_warns: Vec<_> = warns
2752            .iter()
2753            .filter(|w| w.contains("Match pattern type mismatch"))
2754            .collect();
2755        assert_eq!(pattern_warns.len(), 1);
2756        assert!(pattern_warns[0].contains("matching int against string literal"));
2757    }
2758
2759    #[test]
2760    fn test_match_pattern_int_against_string() {
2761        let warns = warnings(
2762            r#"pipeline t(task) {
2763  let x: string = "hello"
2764  match x {
2765    42 -> { log("bad") }
2766    "hello" -> { log("ok") }
2767  }
2768}"#,
2769        );
2770        let pattern_warns: Vec<_> = warns
2771            .iter()
2772            .filter(|w| w.contains("Match pattern type mismatch"))
2773            .collect();
2774        assert_eq!(pattern_warns.len(), 1);
2775        assert!(pattern_warns[0].contains("matching string against int literal"));
2776    }
2777
2778    #[test]
2779    fn test_match_pattern_bool_against_int() {
2780        let warns = warnings(
2781            r#"pipeline t(task) {
2782  let x: int = 42
2783  match x {
2784    true -> { log("bad") }
2785    42 -> { log("ok") }
2786  }
2787}"#,
2788        );
2789        let pattern_warns: Vec<_> = warns
2790            .iter()
2791            .filter(|w| w.contains("Match pattern type mismatch"))
2792            .collect();
2793        assert_eq!(pattern_warns.len(), 1);
2794        assert!(pattern_warns[0].contains("matching int against bool literal"));
2795    }
2796
2797    #[test]
2798    fn test_match_pattern_float_against_string() {
2799        let warns = warnings(
2800            r#"pipeline t(task) {
2801  let x: string = "hello"
2802  match x {
2803    3.14 -> { log("bad") }
2804    "hello" -> { log("ok") }
2805  }
2806}"#,
2807        );
2808        let pattern_warns: Vec<_> = warns
2809            .iter()
2810            .filter(|w| w.contains("Match pattern type mismatch"))
2811            .collect();
2812        assert_eq!(pattern_warns.len(), 1);
2813        assert!(pattern_warns[0].contains("matching string against float literal"));
2814    }
2815
2816    #[test]
2817    fn test_match_pattern_int_against_float_ok() {
2818        // int and float are compatible for match patterns
2819        let warns = warnings(
2820            r#"pipeline t(task) {
2821  let x: float = 3.14
2822  match x {
2823    42 -> { log("ok") }
2824    _ -> { log("default") }
2825  }
2826}"#,
2827        );
2828        let pattern_warns: Vec<_> = warns
2829            .iter()
2830            .filter(|w| w.contains("Match pattern type mismatch"))
2831            .collect();
2832        assert!(pattern_warns.is_empty());
2833    }
2834
2835    #[test]
2836    fn test_match_pattern_float_against_int_ok() {
2837        // float and int are compatible for match patterns
2838        let warns = warnings(
2839            r#"pipeline t(task) {
2840  let x: int = 42
2841  match x {
2842    3.14 -> { log("close") }
2843    _ -> { log("default") }
2844  }
2845}"#,
2846        );
2847        let pattern_warns: Vec<_> = warns
2848            .iter()
2849            .filter(|w| w.contains("Match pattern type mismatch"))
2850            .collect();
2851        assert!(pattern_warns.is_empty());
2852    }
2853
2854    #[test]
2855    fn test_match_pattern_correct_types_no_warning() {
2856        let warns = warnings(
2857            r#"pipeline t(task) {
2858  let x: int = 42
2859  match x {
2860    1 -> { log("one") }
2861    2 -> { log("two") }
2862    _ -> { log("other") }
2863  }
2864}"#,
2865        );
2866        let pattern_warns: Vec<_> = warns
2867            .iter()
2868            .filter(|w| w.contains("Match pattern type mismatch"))
2869            .collect();
2870        assert!(pattern_warns.is_empty());
2871    }
2872
2873    #[test]
2874    fn test_match_pattern_wildcard_no_warning() {
2875        let warns = warnings(
2876            r#"pipeline t(task) {
2877  let x: int = 42
2878  match x {
2879    _ -> { log("catch all") }
2880  }
2881}"#,
2882        );
2883        let pattern_warns: Vec<_> = warns
2884            .iter()
2885            .filter(|w| w.contains("Match pattern type mismatch"))
2886            .collect();
2887        assert!(pattern_warns.is_empty());
2888    }
2889
2890    #[test]
2891    fn test_match_pattern_untyped_no_warning() {
2892        // When value has no known type, no warning should be emitted
2893        let warns = warnings(
2894            r#"pipeline t(task) {
2895  let x = some_unknown_fn()
2896  match x {
2897    "hello" -> { log("string") }
2898    42 -> { log("int") }
2899  }
2900}"#,
2901        );
2902        let pattern_warns: Vec<_> = warns
2903            .iter()
2904            .filter(|w| w.contains("Match pattern type mismatch"))
2905            .collect();
2906        assert!(pattern_warns.is_empty());
2907    }
2908
2909    // --- Interface constraint type checking tests ---
2910
2911    fn iface_warns(source: &str) -> Vec<String> {
2912        warnings(source)
2913            .into_iter()
2914            .filter(|w| w.contains("does not satisfy interface"))
2915            .collect()
2916    }
2917
2918    #[test]
2919    fn test_interface_constraint_return_type_mismatch() {
2920        let warns = iface_warns(
2921            r#"pipeline t(task) {
2922  interface Sizable {
2923    fn size(self) -> int
2924  }
2925  struct Box { width: int }
2926  impl Box {
2927    fn size(self) -> string { return "nope" }
2928  }
2929  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2930  measure(Box({width: 3}))
2931}"#,
2932        );
2933        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2934        assert!(
2935            warns[0].contains("method 'size' returns 'string', expected 'int'"),
2936            "unexpected message: {}",
2937            warns[0]
2938        );
2939    }
2940
2941    #[test]
2942    fn test_interface_constraint_param_type_mismatch() {
2943        let warns = iface_warns(
2944            r#"pipeline t(task) {
2945  interface Processor {
2946    fn process(self, x: int) -> string
2947  }
2948  struct MyProc { name: string }
2949  impl MyProc {
2950    fn process(self, x: string) -> string { return x }
2951  }
2952  fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
2953  run_proc(MyProc({name: "a"}))
2954}"#,
2955        );
2956        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2957        assert!(
2958            warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
2959            "unexpected message: {}",
2960            warns[0]
2961        );
2962    }
2963
2964    #[test]
2965    fn test_interface_constraint_missing_method() {
2966        let warns = iface_warns(
2967            r#"pipeline t(task) {
2968  interface Sizable {
2969    fn size(self) -> int
2970  }
2971  struct Box { width: int }
2972  impl Box {
2973    fn area(self) -> int { return self.width }
2974  }
2975  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2976  measure(Box({width: 3}))
2977}"#,
2978        );
2979        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2980        assert!(
2981            warns[0].contains("missing method 'size'"),
2982            "unexpected message: {}",
2983            warns[0]
2984        );
2985    }
2986
2987    #[test]
2988    fn test_interface_constraint_param_count_mismatch() {
2989        let warns = iface_warns(
2990            r#"pipeline t(task) {
2991  interface Doubler {
2992    fn double(self, x: int) -> int
2993  }
2994  struct Bad { v: int }
2995  impl Bad {
2996    fn double(self) -> int { return self.v * 2 }
2997  }
2998  fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
2999  run_double(Bad({v: 5}))
3000}"#,
3001        );
3002        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3003        assert!(
3004            warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
3005            "unexpected message: {}",
3006            warns[0]
3007        );
3008    }
3009
3010    #[test]
3011    fn test_interface_constraint_satisfied() {
3012        let warns = iface_warns(
3013            r#"pipeline t(task) {
3014  interface Sizable {
3015    fn size(self) -> int
3016  }
3017  struct Box { width: int, height: int }
3018  impl Box {
3019    fn size(self) -> int { return self.width * self.height }
3020  }
3021  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3022  measure(Box({width: 3, height: 4}))
3023}"#,
3024        );
3025        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3026    }
3027
3028    #[test]
3029    fn test_interface_constraint_untyped_impl_compatible() {
3030        // Gradual typing: untyped impl return should not trigger warning
3031        let warns = iface_warns(
3032            r#"pipeline t(task) {
3033  interface Sizable {
3034    fn size(self) -> int
3035  }
3036  struct Box { width: int }
3037  impl Box {
3038    fn size(self) { return self.width }
3039  }
3040  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3041  measure(Box({width: 3}))
3042}"#,
3043        );
3044        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3045    }
3046
3047    #[test]
3048    fn test_interface_constraint_int_float_covariance() {
3049        // int is compatible with float (covariance)
3050        let warns = iface_warns(
3051            r#"pipeline t(task) {
3052  interface Measurable {
3053    fn value(self) -> float
3054  }
3055  struct Gauge { v: int }
3056  impl Gauge {
3057    fn value(self) -> int { return self.v }
3058  }
3059  fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
3060  read_val(Gauge({v: 42}))
3061}"#,
3062        );
3063        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3064    }
3065
3066    // --- Flow-sensitive type refinement tests ---
3067
3068    #[test]
3069    fn test_nil_narrowing_then_branch() {
3070        // Existing behavior: x != nil narrows to string in then-branch
3071        let errs = errors(
3072            r#"pipeline t(task) {
3073  fn greet(name: string | nil) {
3074    if name != nil {
3075      let s: string = name
3076    }
3077  }
3078}"#,
3079        );
3080        assert!(errs.is_empty(), "got: {:?}", errs);
3081    }
3082
3083    #[test]
3084    fn test_nil_narrowing_else_branch() {
3085        // NEW: x != nil narrows to nil in else-branch
3086        let errs = errors(
3087            r#"pipeline t(task) {
3088  fn check(x: string | nil) {
3089    if x != nil {
3090      let s: string = x
3091    } else {
3092      let n: nil = x
3093    }
3094  }
3095}"#,
3096        );
3097        assert!(errs.is_empty(), "got: {:?}", errs);
3098    }
3099
3100    #[test]
3101    fn test_nil_equality_narrows_both() {
3102        // x == nil narrows then to nil, else to non-nil
3103        let errs = errors(
3104            r#"pipeline t(task) {
3105  fn check(x: string | nil) {
3106    if x == nil {
3107      let n: nil = x
3108    } else {
3109      let s: string = x
3110    }
3111  }
3112}"#,
3113        );
3114        assert!(errs.is_empty(), "got: {:?}", errs);
3115    }
3116
3117    #[test]
3118    fn test_truthiness_narrowing() {
3119        // Bare identifier in condition removes nil
3120        let errs = errors(
3121            r#"pipeline t(task) {
3122  fn check(x: string | nil) {
3123    if x {
3124      let s: string = x
3125    }
3126  }
3127}"#,
3128        );
3129        assert!(errs.is_empty(), "got: {:?}", errs);
3130    }
3131
3132    #[test]
3133    fn test_negation_narrowing() {
3134        // !x swaps truthy/falsy
3135        let errs = errors(
3136            r#"pipeline t(task) {
3137  fn check(x: string | nil) {
3138    if !x {
3139      let n: nil = x
3140    } else {
3141      let s: string = x
3142    }
3143  }
3144}"#,
3145        );
3146        assert!(errs.is_empty(), "got: {:?}", errs);
3147    }
3148
3149    #[test]
3150    fn test_typeof_narrowing() {
3151        // type_of(x) == "string" narrows to string
3152        let errs = errors(
3153            r#"pipeline t(task) {
3154  fn check(x: string | int) {
3155    if type_of(x) == "string" {
3156      let s: string = x
3157    }
3158  }
3159}"#,
3160        );
3161        assert!(errs.is_empty(), "got: {:?}", errs);
3162    }
3163
3164    #[test]
3165    fn test_typeof_narrowing_else() {
3166        // else removes the tested type
3167        let errs = errors(
3168            r#"pipeline t(task) {
3169  fn check(x: string | int) {
3170    if type_of(x) == "string" {
3171      let s: string = x
3172    } else {
3173      let i: int = x
3174    }
3175  }
3176}"#,
3177        );
3178        assert!(errs.is_empty(), "got: {:?}", errs);
3179    }
3180
3181    #[test]
3182    fn test_typeof_neq_narrowing() {
3183        // type_of(x) != "string" removes string in then, narrows to string in else
3184        let errs = errors(
3185            r#"pipeline t(task) {
3186  fn check(x: string | int) {
3187    if type_of(x) != "string" {
3188      let i: int = x
3189    } else {
3190      let s: string = x
3191    }
3192  }
3193}"#,
3194        );
3195        assert!(errs.is_empty(), "got: {:?}", errs);
3196    }
3197
3198    #[test]
3199    fn test_and_combines_narrowing() {
3200        // && combines truthy refinements
3201        let errs = errors(
3202            r#"pipeline t(task) {
3203  fn check(x: string | int | nil) {
3204    if x != nil && type_of(x) == "string" {
3205      let s: string = x
3206    }
3207  }
3208}"#,
3209        );
3210        assert!(errs.is_empty(), "got: {:?}", errs);
3211    }
3212
3213    #[test]
3214    fn test_or_falsy_narrowing() {
3215        // || combines falsy refinements
3216        let errs = errors(
3217            r#"pipeline t(task) {
3218  fn check(x: string | nil, y: int | nil) {
3219    if x || y {
3220      // conservative: can't narrow
3221    } else {
3222      let xn: nil = x
3223      let yn: nil = y
3224    }
3225  }
3226}"#,
3227        );
3228        assert!(errs.is_empty(), "got: {:?}", errs);
3229    }
3230
3231    #[test]
3232    fn test_guard_narrows_outer_scope() {
3233        let errs = errors(
3234            r#"pipeline t(task) {
3235  fn check(x: string | nil) {
3236    guard x != nil else { return }
3237    let s: string = x
3238  }
3239}"#,
3240        );
3241        assert!(errs.is_empty(), "got: {:?}", errs);
3242    }
3243
3244    #[test]
3245    fn test_while_narrows_body() {
3246        let errs = errors(
3247            r#"pipeline t(task) {
3248  fn check(x: string | nil) {
3249    while x != nil {
3250      let s: string = x
3251      break
3252    }
3253  }
3254}"#,
3255        );
3256        assert!(errs.is_empty(), "got: {:?}", errs);
3257    }
3258
3259    #[test]
3260    fn test_early_return_narrows_after_if() {
3261        // if then-body returns, falsy refinements apply after
3262        let errs = errors(
3263            r#"pipeline t(task) {
3264  fn check(x: string | nil) -> string {
3265    if x == nil {
3266      return "default"
3267    }
3268    let s: string = x
3269    return s
3270  }
3271}"#,
3272        );
3273        assert!(errs.is_empty(), "got: {:?}", errs);
3274    }
3275
3276    #[test]
3277    fn test_early_throw_narrows_after_if() {
3278        let errs = errors(
3279            r#"pipeline t(task) {
3280  fn check(x: string | nil) {
3281    if x == nil {
3282      throw "missing"
3283    }
3284    let s: string = x
3285  }
3286}"#,
3287        );
3288        assert!(errs.is_empty(), "got: {:?}", errs);
3289    }
3290
3291    #[test]
3292    fn test_no_narrowing_unknown_type() {
3293        // Gradual typing: untyped vars don't get narrowed
3294        let errs = errors(
3295            r#"pipeline t(task) {
3296  fn check(x) {
3297    if x != nil {
3298      let s: string = x
3299    }
3300  }
3301}"#,
3302        );
3303        // No narrowing possible, so assigning untyped x to string should be fine
3304        // (gradual typing allows it)
3305        assert!(errs.is_empty(), "got: {:?}", errs);
3306    }
3307
3308    #[test]
3309    fn test_reassignment_invalidates_narrowing() {
3310        // After reassigning a narrowed var, the original type should be restored
3311        let errs = errors(
3312            r#"pipeline t(task) {
3313  fn check(x: string | nil) {
3314    var y: string | nil = x
3315    if y != nil {
3316      let s: string = y
3317      y = nil
3318      let s2: string = y
3319    }
3320  }
3321}"#,
3322        );
3323        // s2 should fail because y was reassigned, invalidating the narrowing
3324        assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
3325        assert!(
3326            errs[0].contains("Type mismatch"),
3327            "expected type mismatch, got: {}",
3328            errs[0]
3329        );
3330    }
3331
3332    #[test]
3333    fn test_let_immutable_warning() {
3334        let all = check_source(
3335            r#"pipeline t(task) {
3336  let x = 42
3337  x = 43
3338}"#,
3339        );
3340        let warnings: Vec<_> = all
3341            .iter()
3342            .filter(|d| d.severity == DiagnosticSeverity::Warning)
3343            .collect();
3344        assert!(
3345            warnings.iter().any(|w| w.message.contains("immutable")),
3346            "expected immutability warning, got: {:?}",
3347            warnings
3348        );
3349    }
3350
3351    #[test]
3352    fn test_nested_narrowing() {
3353        let errs = errors(
3354            r#"pipeline t(task) {
3355  fn check(x: string | int | nil) {
3356    if x != nil {
3357      if type_of(x) == "int" {
3358        let i: int = x
3359      }
3360    }
3361  }
3362}"#,
3363        );
3364        assert!(errs.is_empty(), "got: {:?}", errs);
3365    }
3366
3367    #[test]
3368    fn test_match_narrows_arms() {
3369        let errs = errors(
3370            r#"pipeline t(task) {
3371  fn check(x: string | int) {
3372    match x {
3373      "hello" -> {
3374        let s: string = x
3375      }
3376      42 -> {
3377        let i: int = x
3378      }
3379      _ -> {}
3380    }
3381  }
3382}"#,
3383        );
3384        assert!(errs.is_empty(), "got: {:?}", errs);
3385    }
3386
3387    #[test]
3388    fn test_has_narrows_optional_field() {
3389        let errs = errors(
3390            r#"pipeline t(task) {
3391  fn check(x: {name?: string, age: int}) {
3392    if x.has("name") {
3393      let n: {name: string, age: int} = x
3394    }
3395  }
3396}"#,
3397        );
3398        assert!(errs.is_empty(), "got: {:?}", errs);
3399    }
3400}