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.error_at(
829                                    format!(
830                                        "Operator '{}' requires numeric operands, got {} and {}",
831                                        op, 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.error_at(
845                                    format!(
846                                        "Operator '*' requires numeric operands or string * int, got {} and {}",
847                                        l, r
848                                    ),
849                                    span,
850                                );
851                            }
852                        }
853                        "+" => {
854                            let valid = matches!(
855                                (l.as_str(), r.as_str()),
856                                ("int" | "float", "int" | "float")
857                                    | ("string", "string")
858                                    | ("list", "list")
859                                    | ("dict", "dict")
860                            );
861                            if !valid {
862                                self.error_at(
863                                    format!("Operator '+' is not valid for types {} and {}", l, r),
864                                    span,
865                                );
866                            }
867                        }
868                        "<" | ">" | "<=" | ">=" => {
869                            let comparable = ["int", "float", "string"];
870                            if !comparable.contains(&l.as_str())
871                                || !comparable.contains(&r.as_str())
872                            {
873                                self.warning_at(
874                                    format!(
875                                        "Comparison '{}' may not be meaningful for types {} and {}",
876                                        op, l, r
877                                    ),
878                                    span,
879                                );
880                            } else if (l == "string") != (r == "string") {
881                                self.warning_at(
882                                    format!(
883                                        "Comparing {} with {} using '{}' may give unexpected results",
884                                        l, r, op
885                                    ),
886                                    span,
887                                );
888                            }
889                        }
890                        _ => {}
891                    }
892                }
893            }
894            Node::UnaryOp { operand, .. } => {
895                self.check_node(operand, scope);
896            }
897            Node::MethodCall {
898                object,
899                method,
900                args,
901                ..
902            }
903            | Node::OptionalMethodCall {
904                object,
905                method,
906                args,
907                ..
908            } => {
909                self.check_node(object, scope);
910                for arg in args {
911                    self.check_node(arg, scope);
912                }
913                // Definition-site generic checking: if the object's type is a
914                // constrained generic param (where T: Interface), verify the
915                // method exists in the bound interface.
916                if let Some(TypeExpr::Named(type_name)) = self.infer_type(object, scope) {
917                    if scope.is_generic_type_param(&type_name) {
918                        if let Some(iface_name) = scope.get_where_constraint(&type_name) {
919                            if let Some(iface_methods) = scope.get_interface(iface_name) {
920                                let has_method = iface_methods.iter().any(|m| m.name == *method);
921                                if !has_method {
922                                    self.warning_at(
923                                        format!(
924                                            "Method '{}' not found in interface '{}' (constraint on '{}')",
925                                            method, iface_name, type_name
926                                        ),
927                                        span,
928                                    );
929                                }
930                            }
931                        }
932                    }
933                }
934            }
935            Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
936                self.check_node(object, scope);
937            }
938            Node::SubscriptAccess { object, index } => {
939                self.check_node(object, scope);
940                self.check_node(index, scope);
941            }
942            Node::SliceAccess { object, start, end } => {
943                self.check_node(object, scope);
944                if let Some(s) = start {
945                    self.check_node(s, scope);
946                }
947                if let Some(e) = end {
948                    self.check_node(e, scope);
949                }
950            }
951
952            // --- Compound nodes: recurse into children ---
953            Node::Ternary {
954                condition,
955                true_expr,
956                false_expr,
957            } => {
958                self.check_node(condition, scope);
959                let refs = Self::extract_refinements(condition, scope);
960
961                let mut true_scope = scope.child();
962                apply_refinements(&mut true_scope, &refs.truthy);
963                self.check_node(true_expr, &mut true_scope);
964
965                let mut false_scope = scope.child();
966                apply_refinements(&mut false_scope, &refs.falsy);
967                self.check_node(false_expr, &mut false_scope);
968            }
969
970            Node::ThrowStmt { value } => {
971                self.check_node(value, scope);
972            }
973
974            Node::GuardStmt {
975                condition,
976                else_body,
977            } => {
978                self.check_node(condition, scope);
979                let refs = Self::extract_refinements(condition, scope);
980
981                let mut else_scope = scope.child();
982                apply_refinements(&mut else_scope, &refs.falsy);
983                self.check_block(else_body, &mut else_scope);
984
985                // After guard, condition is true — apply truthy refinements
986                // to the OUTER scope (guard's else-body must exit)
987                apply_refinements(scope, &refs.truthy);
988            }
989
990            Node::SpawnExpr { body } => {
991                let mut spawn_scope = scope.child();
992                self.check_block(body, &mut spawn_scope);
993            }
994
995            Node::Parallel {
996                count,
997                variable,
998                body,
999            } => {
1000                self.check_node(count, scope);
1001                let mut par_scope = scope.child();
1002                if let Some(var) = variable {
1003                    par_scope.define_var(var, Some(TypeExpr::Named("int".into())));
1004                }
1005                self.check_block(body, &mut par_scope);
1006            }
1007
1008            Node::ParallelMap {
1009                list,
1010                variable,
1011                body,
1012            }
1013            | Node::ParallelSettle {
1014                list,
1015                variable,
1016                body,
1017            } => {
1018                self.check_node(list, scope);
1019                let mut par_scope = scope.child();
1020                let elem_type = match self.infer_type(list, scope) {
1021                    Some(TypeExpr::List(inner)) => Some(*inner),
1022                    _ => None,
1023                };
1024                par_scope.define_var(variable, elem_type);
1025                self.check_block(body, &mut par_scope);
1026            }
1027
1028            Node::SelectExpr {
1029                cases,
1030                timeout,
1031                default_body,
1032            } => {
1033                for case in cases {
1034                    self.check_node(&case.channel, scope);
1035                    let mut case_scope = scope.child();
1036                    case_scope.define_var(&case.variable, None);
1037                    self.check_block(&case.body, &mut case_scope);
1038                }
1039                if let Some((dur, body)) = timeout {
1040                    self.check_node(dur, scope);
1041                    let mut timeout_scope = scope.child();
1042                    self.check_block(body, &mut timeout_scope);
1043                }
1044                if let Some(body) = default_body {
1045                    let mut default_scope = scope.child();
1046                    self.check_block(body, &mut default_scope);
1047                }
1048            }
1049
1050            Node::DeadlineBlock { duration, body } => {
1051                self.check_node(duration, scope);
1052                let mut block_scope = scope.child();
1053                self.check_block(body, &mut block_scope);
1054            }
1055
1056            Node::MutexBlock { body } => {
1057                let mut block_scope = scope.child();
1058                self.check_block(body, &mut block_scope);
1059            }
1060
1061            Node::Retry { count, body } => {
1062                self.check_node(count, scope);
1063                let mut retry_scope = scope.child();
1064                self.check_block(body, &mut retry_scope);
1065            }
1066
1067            Node::Closure { params, body, .. } => {
1068                let mut closure_scope = scope.child();
1069                for p in params {
1070                    closure_scope.define_var(&p.name, p.type_expr.clone());
1071                }
1072                self.check_block(body, &mut closure_scope);
1073            }
1074
1075            Node::ListLiteral(elements) => {
1076                for elem in elements {
1077                    self.check_node(elem, scope);
1078                }
1079            }
1080
1081            Node::DictLiteral(entries) | Node::AskExpr { fields: entries } => {
1082                for entry in entries {
1083                    self.check_node(&entry.key, scope);
1084                    self.check_node(&entry.value, scope);
1085                }
1086            }
1087
1088            Node::RangeExpr { start, end, .. } => {
1089                self.check_node(start, scope);
1090                self.check_node(end, scope);
1091            }
1092
1093            Node::Spread(inner) => {
1094                self.check_node(inner, scope);
1095            }
1096
1097            Node::Block(stmts) => {
1098                let mut block_scope = scope.child();
1099                self.check_block(stmts, &mut block_scope);
1100            }
1101
1102            Node::YieldExpr { value } => {
1103                if let Some(v) = value {
1104                    self.check_node(v, scope);
1105                }
1106            }
1107
1108            // --- Struct construction: validate fields against declaration ---
1109            Node::StructConstruct {
1110                struct_name,
1111                fields,
1112            } => {
1113                for entry in fields {
1114                    self.check_node(&entry.key, scope);
1115                    self.check_node(&entry.value, scope);
1116                }
1117                if let Some(declared_fields) = scope.get_struct(struct_name).cloned() {
1118                    // Warn on unknown fields
1119                    for entry in fields {
1120                        if let Node::StringLiteral(key) | Node::Identifier(key) = &entry.key.node {
1121                            if !declared_fields.iter().any(|(name, _)| name == key) {
1122                                self.warning_at(
1123                                    format!("Unknown field '{}' in struct '{}'", key, struct_name),
1124                                    entry.key.span,
1125                                );
1126                            }
1127                        }
1128                    }
1129                    // Warn on missing required fields
1130                    let provided: Vec<String> = fields
1131                        .iter()
1132                        .filter_map(|e| match &e.key.node {
1133                            Node::StringLiteral(k) | Node::Identifier(k) => Some(k.clone()),
1134                            _ => None,
1135                        })
1136                        .collect();
1137                    for (name, _) in &declared_fields {
1138                        if !provided.contains(name) {
1139                            self.warning_at(
1140                                format!(
1141                                    "Missing field '{}' in struct '{}' construction",
1142                                    name, struct_name
1143                                ),
1144                                span,
1145                            );
1146                        }
1147                    }
1148                }
1149            }
1150
1151            // --- Enum construction: validate variant exists ---
1152            Node::EnumConstruct {
1153                enum_name,
1154                variant,
1155                args,
1156            } => {
1157                for arg in args {
1158                    self.check_node(arg, scope);
1159                }
1160                if let Some(variants) = scope.get_enum(enum_name) {
1161                    if !variants.contains(variant) {
1162                        self.warning_at(
1163                            format!("Unknown variant '{}' in enum '{}'", variant, enum_name),
1164                            span,
1165                        );
1166                    }
1167                }
1168            }
1169
1170            // --- InterpolatedString: segments are lexer-level, no SNode children ---
1171            Node::InterpolatedString(_) => {}
1172
1173            // --- Terminals: no children to check ---
1174            Node::StringLiteral(_)
1175            | Node::RawStringLiteral(_)
1176            | Node::IntLiteral(_)
1177            | Node::FloatLiteral(_)
1178            | Node::BoolLiteral(_)
1179            | Node::NilLiteral
1180            | Node::Identifier(_)
1181            | Node::DurationLiteral(_)
1182            | Node::BreakStmt
1183            | Node::ContinueStmt
1184            | Node::ReturnStmt { value: None }
1185            | Node::ImportDecl { .. }
1186            | Node::SelectiveImport { .. } => {}
1187
1188            // Declarations already handled above; catch remaining variants
1189            // that have no meaningful type-check behavior.
1190            Node::Pipeline { body, .. } | Node::OverrideDecl { body, .. } => {
1191                let mut decl_scope = scope.child();
1192                self.check_block(body, &mut decl_scope);
1193            }
1194        }
1195    }
1196
1197    fn check_fn_body(
1198        &mut self,
1199        type_params: &[TypeParam],
1200        params: &[TypedParam],
1201        return_type: &Option<TypeExpr>,
1202        body: &[SNode],
1203        where_clauses: &[WhereClause],
1204    ) {
1205        let mut fn_scope = self.scope.child();
1206        // Register generic type parameters so they are treated as compatible
1207        // with any concrete type during type checking.
1208        for tp in type_params {
1209            fn_scope.generic_type_params.insert(tp.name.clone());
1210        }
1211        // Store where-clause constraints for definition-site checking
1212        for wc in where_clauses {
1213            fn_scope
1214                .where_constraints
1215                .insert(wc.type_name.clone(), wc.bound.clone());
1216        }
1217        for param in params {
1218            fn_scope.define_var(&param.name, param.type_expr.clone());
1219            if let Some(default) = &param.default_value {
1220                self.check_node(default, &mut fn_scope);
1221            }
1222        }
1223        // Snapshot scope before main pass (which may mutate it with narrowing)
1224        // so that return-type checking starts from the original parameter types.
1225        let ret_scope_base = if return_type.is_some() {
1226            Some(fn_scope.child())
1227        } else {
1228            None
1229        };
1230
1231        self.check_block(body, &mut fn_scope);
1232
1233        // Check return statements against declared return type
1234        if let Some(ret_type) = return_type {
1235            let mut ret_scope = ret_scope_base.unwrap();
1236            for stmt in body {
1237                self.check_return_type(stmt, ret_type, &mut ret_scope);
1238            }
1239        }
1240    }
1241
1242    fn check_return_type(&mut self, snode: &SNode, expected: &TypeExpr, scope: &mut TypeScope) {
1243        let span = snode.span;
1244        match &snode.node {
1245            Node::ReturnStmt { value: Some(val) } => {
1246                let inferred = self.infer_type(val, scope);
1247                if let Some(actual) = &inferred {
1248                    if !self.types_compatible(expected, actual, scope) {
1249                        self.error_at(
1250                            format!(
1251                                "Return type mismatch: expected {}, got {}",
1252                                format_type(expected),
1253                                format_type(actual)
1254                            ),
1255                            span,
1256                        );
1257                    }
1258                }
1259            }
1260            Node::IfElse {
1261                condition,
1262                then_body,
1263                else_body,
1264            } => {
1265                let refs = Self::extract_refinements(condition, scope);
1266                let mut then_scope = scope.child();
1267                apply_refinements(&mut then_scope, &refs.truthy);
1268                for stmt in then_body {
1269                    self.check_return_type(stmt, expected, &mut then_scope);
1270                }
1271                if let Some(else_body) = else_body {
1272                    let mut else_scope = scope.child();
1273                    apply_refinements(&mut else_scope, &refs.falsy);
1274                    for stmt in else_body {
1275                        self.check_return_type(stmt, expected, &mut else_scope);
1276                    }
1277                    // Post-branch narrowing for return type checking
1278                    if Self::block_definitely_exits(then_body)
1279                        && !Self::block_definitely_exits(else_body)
1280                    {
1281                        apply_refinements(scope, &refs.falsy);
1282                    } else if Self::block_definitely_exits(else_body)
1283                        && !Self::block_definitely_exits(then_body)
1284                    {
1285                        apply_refinements(scope, &refs.truthy);
1286                    }
1287                } else {
1288                    // No else: if then-body always exits, apply falsy after
1289                    if Self::block_definitely_exits(then_body) {
1290                        apply_refinements(scope, &refs.falsy);
1291                    }
1292                }
1293            }
1294            _ => {}
1295        }
1296    }
1297
1298    /// Check if a match expression on an enum's `.variant` property covers all variants.
1299    /// Extract narrowing info from nil-check conditions like `x != nil`.
1300    /// Returns (var_name, narrowed_type) where narrowed_type removes nil from a union.
1301    /// Check if a type satisfies an interface (Go-style implicit satisfaction).
1302    /// A type satisfies an interface if its impl block has all the required methods.
1303    fn satisfies_interface(
1304        &self,
1305        type_name: &str,
1306        interface_name: &str,
1307        scope: &TypeScope,
1308    ) -> bool {
1309        self.interface_mismatch_reason(type_name, interface_name, scope)
1310            .is_none()
1311    }
1312
1313    /// Return a detailed reason why a type does not satisfy an interface, or None
1314    /// if it does satisfy it.  Used for producing actionable warning messages.
1315    fn interface_mismatch_reason(
1316        &self,
1317        type_name: &str,
1318        interface_name: &str,
1319        scope: &TypeScope,
1320    ) -> Option<String> {
1321        let interface_methods = match scope.get_interface(interface_name) {
1322            Some(methods) => methods,
1323            None => return Some(format!("interface '{}' not found", interface_name)),
1324        };
1325        let impl_methods = match scope.get_impl_methods(type_name) {
1326            Some(methods) => methods,
1327            None => {
1328                if interface_methods.is_empty() {
1329                    return None;
1330                }
1331                let names: Vec<_> = interface_methods.iter().map(|m| m.name.as_str()).collect();
1332                return Some(format!("missing method(s): {}", names.join(", ")));
1333            }
1334        };
1335        for iface_method in interface_methods {
1336            let iface_params: Vec<_> = iface_method
1337                .params
1338                .iter()
1339                .filter(|p| p.name != "self")
1340                .collect();
1341            let iface_param_count = iface_params.len();
1342            let matching_impl = impl_methods.iter().find(|im| im.name == iface_method.name);
1343            let impl_method = match matching_impl {
1344                Some(m) => m,
1345                None => {
1346                    return Some(format!("missing method '{}'", iface_method.name));
1347                }
1348            };
1349            if impl_method.param_count != iface_param_count {
1350                return Some(format!(
1351                    "method '{}' has {} parameter(s), expected {}",
1352                    iface_method.name, impl_method.param_count, iface_param_count
1353                ));
1354            }
1355            // Check parameter types where both sides specify them
1356            for (i, iface_param) in iface_params.iter().enumerate() {
1357                if let (Some(expected), Some(actual)) = (
1358                    &iface_param.type_expr,
1359                    impl_method.param_types.get(i).and_then(|t| t.as_ref()),
1360                ) {
1361                    if !self.types_compatible(expected, actual, scope) {
1362                        return Some(format!(
1363                            "method '{}' parameter {} has type '{}', expected '{}'",
1364                            iface_method.name,
1365                            i + 1,
1366                            format_type(actual),
1367                            format_type(expected),
1368                        ));
1369                    }
1370                }
1371            }
1372            // Check return type where both sides specify it
1373            if let (Some(expected_ret), Some(actual_ret)) =
1374                (&iface_method.return_type, &impl_method.return_type)
1375            {
1376                if !self.types_compatible(expected_ret, actual_ret, scope) {
1377                    return Some(format!(
1378                        "method '{}' returns '{}', expected '{}'",
1379                        iface_method.name,
1380                        format_type(actual_ret),
1381                        format_type(expected_ret),
1382                    ));
1383                }
1384            }
1385        }
1386        None
1387    }
1388
1389    /// Recursively extract type parameter bindings from matching param/arg types.
1390    /// E.g., param_type=list<T> + arg_type=list<Dog> → binds T=Dog.
1391    fn extract_type_bindings(
1392        param_type: &TypeExpr,
1393        arg_type: &TypeExpr,
1394        type_params: &std::collections::BTreeSet<String>,
1395        bindings: &mut BTreeMap<String, String>,
1396    ) {
1397        match (param_type, arg_type) {
1398            // Direct type param match: T → concrete
1399            (TypeExpr::Named(param_name), TypeExpr::Named(concrete))
1400                if type_params.contains(param_name) =>
1401            {
1402                bindings
1403                    .entry(param_name.clone())
1404                    .or_insert(concrete.clone());
1405            }
1406            // list<T> + list<Dog>
1407            (TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
1408                Self::extract_type_bindings(p_inner, a_inner, type_params, bindings);
1409            }
1410            // dict<K, V> + dict<string, int>
1411            (TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
1412                Self::extract_type_bindings(pk, ak, type_params, bindings);
1413                Self::extract_type_bindings(pv, av, type_params, bindings);
1414            }
1415            _ => {}
1416        }
1417    }
1418
1419    /// Extract bidirectional type refinements from a condition expression.
1420    fn extract_refinements(condition: &SNode, scope: &TypeScope) -> Refinements {
1421        match &condition.node {
1422            // --- Nil checks and type_of checks ---
1423            Node::BinaryOp { op, left, right } if op == "!=" || op == "==" => {
1424                let nil_ref = Self::extract_nil_refinements(op, left, right, scope);
1425                if !nil_ref.truthy.is_empty() || !nil_ref.falsy.is_empty() {
1426                    return nil_ref;
1427                }
1428                let typeof_ref = Self::extract_typeof_refinements(op, left, right, scope);
1429                if !typeof_ref.truthy.is_empty() || !typeof_ref.falsy.is_empty() {
1430                    return typeof_ref;
1431                }
1432                Refinements::empty()
1433            }
1434
1435            // --- Logical AND: both must be true on truthy path ---
1436            Node::BinaryOp { op, left, right } if op == "&&" => {
1437                let left_ref = Self::extract_refinements(left, scope);
1438                let right_ref = Self::extract_refinements(right, scope);
1439                let mut truthy = left_ref.truthy;
1440                truthy.extend(right_ref.truthy);
1441                Refinements {
1442                    truthy,
1443                    falsy: vec![],
1444                }
1445            }
1446
1447            // --- Logical OR: both must be false on falsy path ---
1448            Node::BinaryOp { op, left, right } if op == "||" => {
1449                let left_ref = Self::extract_refinements(left, scope);
1450                let right_ref = Self::extract_refinements(right, scope);
1451                let mut falsy = left_ref.falsy;
1452                falsy.extend(right_ref.falsy);
1453                Refinements {
1454                    truthy: vec![],
1455                    falsy,
1456                }
1457            }
1458
1459            // --- Negation: swap truthy/falsy ---
1460            Node::UnaryOp { op, operand } if op == "!" => {
1461                Self::extract_refinements(operand, scope).inverted()
1462            }
1463
1464            // --- Truthiness: bare identifier in condition position ---
1465            Node::Identifier(name) => {
1466                if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1467                    if members
1468                        .iter()
1469                        .any(|m| matches!(m, TypeExpr::Named(n) if n == "nil"))
1470                    {
1471                        if let Some(narrowed) = remove_from_union(members, "nil") {
1472                            return Refinements {
1473                                truthy: vec![(name.clone(), Some(narrowed))],
1474                                falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1475                            };
1476                        }
1477                    }
1478                }
1479                Refinements::empty()
1480            }
1481
1482            // --- .has("key") on shapes ---
1483            Node::MethodCall {
1484                object,
1485                method,
1486                args,
1487            } if method == "has" && args.len() == 1 => {
1488                Self::extract_has_refinements(object, args, scope)
1489            }
1490
1491            _ => Refinements::empty(),
1492        }
1493    }
1494
1495    /// Extract nil-check refinements from `x != nil` / `x == nil` patterns.
1496    fn extract_nil_refinements(
1497        op: &str,
1498        left: &SNode,
1499        right: &SNode,
1500        scope: &TypeScope,
1501    ) -> Refinements {
1502        let var_node = if matches!(right.node, Node::NilLiteral) {
1503            left
1504        } else if matches!(left.node, Node::NilLiteral) {
1505            right
1506        } else {
1507            return Refinements::empty();
1508        };
1509
1510        if let Node::Identifier(name) = &var_node.node {
1511            if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1512                if let Some(narrowed) = remove_from_union(members, "nil") {
1513                    let neq_refs = Refinements {
1514                        truthy: vec![(name.clone(), Some(narrowed))],
1515                        falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1516                    };
1517                    return if op == "!=" {
1518                        neq_refs
1519                    } else {
1520                        neq_refs.inverted()
1521                    };
1522                }
1523            }
1524        }
1525        Refinements::empty()
1526    }
1527
1528    /// Extract type_of refinements from `type_of(x) == "typename"` patterns.
1529    fn extract_typeof_refinements(
1530        op: &str,
1531        left: &SNode,
1532        right: &SNode,
1533        scope: &TypeScope,
1534    ) -> Refinements {
1535        let (var_name, type_name) = if let (Some(var), Node::StringLiteral(tn)) =
1536            (extract_type_of_var(left), &right.node)
1537        {
1538            (var, tn.clone())
1539        } else if let (Node::StringLiteral(tn), Some(var)) =
1540            (&left.node, extract_type_of_var(right))
1541        {
1542            (var, tn.clone())
1543        } else {
1544            return Refinements::empty();
1545        };
1546
1547        const KNOWN_TYPES: &[&str] = &[
1548            "int", "string", "float", "bool", "nil", "list", "dict", "closure",
1549        ];
1550        if !KNOWN_TYPES.contains(&type_name.as_str()) {
1551            return Refinements::empty();
1552        }
1553
1554        if let Some(Some(TypeExpr::Union(members))) = scope.get_var(&var_name) {
1555            let narrowed = narrow_to_single(members, &type_name);
1556            let remaining = remove_from_union(members, &type_name);
1557            if narrowed.is_some() || remaining.is_some() {
1558                let eq_refs = Refinements {
1559                    truthy: narrowed
1560                        .map(|n| vec![(var_name.clone(), Some(n))])
1561                        .unwrap_or_default(),
1562                    falsy: remaining
1563                        .map(|r| vec![(var_name.clone(), Some(r))])
1564                        .unwrap_or_default(),
1565                };
1566                return if op == "==" {
1567                    eq_refs
1568                } else {
1569                    eq_refs.inverted()
1570                };
1571            }
1572        }
1573        Refinements::empty()
1574    }
1575
1576    /// Extract .has("key") refinements on shape types.
1577    fn extract_has_refinements(object: &SNode, args: &[SNode], scope: &TypeScope) -> Refinements {
1578        if let Node::Identifier(var_name) = &object.node {
1579            if let Node::StringLiteral(key) = &args[0].node {
1580                if let Some(Some(TypeExpr::Shape(fields))) = scope.get_var(var_name) {
1581                    if fields.iter().any(|f| f.name == *key && f.optional) {
1582                        let narrowed_fields: Vec<ShapeField> = fields
1583                            .iter()
1584                            .map(|f| {
1585                                if f.name == *key {
1586                                    ShapeField {
1587                                        name: f.name.clone(),
1588                                        type_expr: f.type_expr.clone(),
1589                                        optional: false,
1590                                    }
1591                                } else {
1592                                    f.clone()
1593                                }
1594                            })
1595                            .collect();
1596                        return Refinements {
1597                            truthy: vec![(
1598                                var_name.clone(),
1599                                Some(TypeExpr::Shape(narrowed_fields)),
1600                            )],
1601                            falsy: vec![],
1602                        };
1603                    }
1604                }
1605            }
1606        }
1607        Refinements::empty()
1608    }
1609
1610    /// Check whether a block definitely exits (return/throw/break/continue).
1611    fn block_definitely_exits(stmts: &[SNode]) -> bool {
1612        stmts.iter().any(|s| match &s.node {
1613            Node::ReturnStmt { .. }
1614            | Node::ThrowStmt { .. }
1615            | Node::BreakStmt
1616            | Node::ContinueStmt => true,
1617            Node::IfElse {
1618                then_body,
1619                else_body: Some(else_body),
1620                ..
1621            } => Self::block_definitely_exits(then_body) && Self::block_definitely_exits(else_body),
1622            _ => false,
1623        })
1624    }
1625
1626    fn check_match_exhaustiveness(
1627        &mut self,
1628        value: &SNode,
1629        arms: &[MatchArm],
1630        scope: &TypeScope,
1631        span: Span,
1632    ) {
1633        // Detect pattern: match <expr>.variant { "VariantA" -> ... }
1634        let enum_name = match &value.node {
1635            Node::PropertyAccess { object, property } if property == "variant" => {
1636                // Infer the type of the object
1637                match self.infer_type(object, scope) {
1638                    Some(TypeExpr::Named(name)) => {
1639                        if scope.get_enum(&name).is_some() {
1640                            Some(name)
1641                        } else {
1642                            None
1643                        }
1644                    }
1645                    _ => None,
1646                }
1647            }
1648            _ => {
1649                // Direct match on an enum value: match <expr> { ... }
1650                match self.infer_type(value, scope) {
1651                    Some(TypeExpr::Named(name)) if scope.get_enum(&name).is_some() => Some(name),
1652                    _ => None,
1653                }
1654            }
1655        };
1656
1657        let Some(enum_name) = enum_name else {
1658            return;
1659        };
1660        let Some(variants) = scope.get_enum(&enum_name) else {
1661            return;
1662        };
1663
1664        // Collect variant names covered by match arms
1665        let mut covered: Vec<String> = Vec::new();
1666        let mut has_wildcard = false;
1667
1668        for arm in arms {
1669            match &arm.pattern.node {
1670                // String literal pattern (matching on .variant): "VariantA"
1671                Node::StringLiteral(s) => covered.push(s.clone()),
1672                // Identifier pattern acts as a wildcard/catch-all
1673                Node::Identifier(name) if name == "_" || !variants.contains(name) => {
1674                    has_wildcard = true;
1675                }
1676                // Direct enum construct pattern: EnumName.Variant
1677                Node::EnumConstruct { variant, .. } => covered.push(variant.clone()),
1678                // PropertyAccess pattern: EnumName.Variant (no args)
1679                Node::PropertyAccess { property, .. } => covered.push(property.clone()),
1680                _ => {
1681                    // Unknown pattern shape — conservatively treat as wildcard
1682                    has_wildcard = true;
1683                }
1684            }
1685        }
1686
1687        if has_wildcard {
1688            return;
1689        }
1690
1691        let missing: Vec<&String> = variants.iter().filter(|v| !covered.contains(v)).collect();
1692        if !missing.is_empty() {
1693            let missing_str = missing
1694                .iter()
1695                .map(|s| format!("\"{}\"", s))
1696                .collect::<Vec<_>>()
1697                .join(", ");
1698            self.warning_at(
1699                format!(
1700                    "Non-exhaustive match on enum {}: missing variants {}",
1701                    enum_name, missing_str
1702                ),
1703                span,
1704            );
1705        }
1706    }
1707
1708    fn check_call(&mut self, name: &str, args: &[SNode], scope: &mut TypeScope, span: Span) {
1709        // Check against known function signatures
1710        let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
1711        if let Some(sig) = scope.get_fn(name).cloned() {
1712            if !has_spread
1713                && !is_builtin(name)
1714                && !sig.has_rest
1715                && (args.len() < sig.required_params || args.len() > sig.params.len())
1716            {
1717                let expected = if sig.required_params == sig.params.len() {
1718                    format!("{}", sig.params.len())
1719                } else {
1720                    format!("{}-{}", sig.required_params, sig.params.len())
1721                };
1722                self.warning_at(
1723                    format!(
1724                        "Function '{}' expects {} arguments, got {}",
1725                        name,
1726                        expected,
1727                        args.len()
1728                    ),
1729                    span,
1730                );
1731            }
1732            // Build a scope that includes the function's generic type params
1733            // so they are treated as compatible with any concrete type.
1734            let call_scope = if sig.type_param_names.is_empty() {
1735                scope.clone()
1736            } else {
1737                let mut s = scope.child();
1738                for tp_name in &sig.type_param_names {
1739                    s.generic_type_params.insert(tp_name.clone());
1740                }
1741                s
1742            };
1743            for (i, (arg, (param_name, param_type))) in
1744                args.iter().zip(sig.params.iter()).enumerate()
1745            {
1746                if let Some(expected) = param_type {
1747                    let actual = self.infer_type(arg, scope);
1748                    if let Some(actual) = &actual {
1749                        if !self.types_compatible(expected, actual, &call_scope) {
1750                            self.error_at(
1751                                format!(
1752                                    "Argument {} ('{}'): expected {}, got {}",
1753                                    i + 1,
1754                                    param_name,
1755                                    format_type(expected),
1756                                    format_type(actual)
1757                                ),
1758                                arg.span,
1759                            );
1760                        }
1761                    }
1762                }
1763            }
1764            // Enforce where-clause constraints at call site
1765            if !sig.where_clauses.is_empty() {
1766                // Build mapping: type_param → concrete type from inferred args.
1767                // Recursively walks Generic types so list<T> + list<Dog> binds T=Dog.
1768                let mut type_bindings: BTreeMap<String, String> = BTreeMap::new();
1769                let type_param_set: std::collections::BTreeSet<String> =
1770                    sig.type_param_names.iter().cloned().collect();
1771                for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
1772                    if let Some(param_ty) = param_type {
1773                        if let Some(arg_ty) = self.infer_type(arg, scope) {
1774                            Self::extract_type_bindings(
1775                                param_ty,
1776                                &arg_ty,
1777                                &type_param_set,
1778                                &mut type_bindings,
1779                            );
1780                        }
1781                    }
1782                }
1783                for (type_param, bound) in &sig.where_clauses {
1784                    if let Some(concrete_type) = type_bindings.get(type_param) {
1785                        if let Some(reason) =
1786                            self.interface_mismatch_reason(concrete_type, bound, scope)
1787                        {
1788                            self.warning_at(
1789                                format!(
1790                                    "Type '{}' does not satisfy interface '{}': {} \
1791                                     (required by constraint `where {}: {}`)",
1792                                    concrete_type, bound, reason, type_param, bound
1793                                ),
1794                                span,
1795                            );
1796                        }
1797                    }
1798                }
1799            }
1800        }
1801        // Check args recursively
1802        for arg in args {
1803            self.check_node(arg, scope);
1804        }
1805    }
1806
1807    /// Infer the type of an expression.
1808    fn infer_type(&self, snode: &SNode, scope: &TypeScope) -> InferredType {
1809        match &snode.node {
1810            Node::IntLiteral(_) => Some(TypeExpr::Named("int".into())),
1811            Node::FloatLiteral(_) => Some(TypeExpr::Named("float".into())),
1812            Node::StringLiteral(_) | Node::InterpolatedString(_) => {
1813                Some(TypeExpr::Named("string".into()))
1814            }
1815            Node::BoolLiteral(_) => Some(TypeExpr::Named("bool".into())),
1816            Node::NilLiteral => Some(TypeExpr::Named("nil".into())),
1817            Node::ListLiteral(_) => Some(TypeExpr::Named("list".into())),
1818            Node::DictLiteral(entries) => {
1819                // Infer shape type when all keys are string literals
1820                let mut fields = Vec::new();
1821                let mut all_string_keys = true;
1822                for entry in entries {
1823                    if let Node::StringLiteral(key) = &entry.key.node {
1824                        let val_type = self
1825                            .infer_type(&entry.value, scope)
1826                            .unwrap_or(TypeExpr::Named("nil".into()));
1827                        fields.push(ShapeField {
1828                            name: key.clone(),
1829                            type_expr: val_type,
1830                            optional: false,
1831                        });
1832                    } else {
1833                        all_string_keys = false;
1834                        break;
1835                    }
1836                }
1837                if all_string_keys && !fields.is_empty() {
1838                    Some(TypeExpr::Shape(fields))
1839                } else {
1840                    Some(TypeExpr::Named("dict".into()))
1841                }
1842            }
1843            Node::Closure { params, body, .. } => {
1844                // If all params are typed and we can infer a return type, produce FnType
1845                let all_typed = params.iter().all(|p| p.type_expr.is_some());
1846                if all_typed && !params.is_empty() {
1847                    let param_types: Vec<TypeExpr> =
1848                        params.iter().filter_map(|p| p.type_expr.clone()).collect();
1849                    // Try to infer return type from last expression in body
1850                    let ret = body.last().and_then(|last| self.infer_type(last, scope));
1851                    if let Some(ret_type) = ret {
1852                        return Some(TypeExpr::FnType {
1853                            params: param_types,
1854                            return_type: Box::new(ret_type),
1855                        });
1856                    }
1857                }
1858                Some(TypeExpr::Named("closure".into()))
1859            }
1860
1861            Node::Identifier(name) => scope.get_var(name).cloned().flatten(),
1862
1863            Node::FunctionCall { name, .. } => {
1864                // Struct constructor calls return the struct type
1865                if scope.get_struct(name).is_some() {
1866                    return Some(TypeExpr::Named(name.clone()));
1867                }
1868                // Check user-defined function return types
1869                if let Some(sig) = scope.get_fn(name) {
1870                    return sig.return_type.clone();
1871                }
1872                // Check builtin return types
1873                builtin_return_type(name)
1874            }
1875
1876            Node::BinaryOp { op, left, right } => {
1877                let lt = self.infer_type(left, scope);
1878                let rt = self.infer_type(right, scope);
1879                infer_binary_op_type(op, &lt, &rt)
1880            }
1881
1882            Node::UnaryOp { op, operand } => {
1883                let t = self.infer_type(operand, scope);
1884                match op.as_str() {
1885                    "!" => Some(TypeExpr::Named("bool".into())),
1886                    "-" => t, // negation preserves type
1887                    _ => None,
1888                }
1889            }
1890
1891            Node::Ternary {
1892                condition,
1893                true_expr,
1894                false_expr,
1895            } => {
1896                let refs = Self::extract_refinements(condition, scope);
1897
1898                let mut true_scope = scope.child();
1899                apply_refinements(&mut true_scope, &refs.truthy);
1900                let tt = self.infer_type(true_expr, &true_scope);
1901
1902                let mut false_scope = scope.child();
1903                apply_refinements(&mut false_scope, &refs.falsy);
1904                let ft = self.infer_type(false_expr, &false_scope);
1905
1906                match (&tt, &ft) {
1907                    (Some(a), Some(b)) if a == b => tt,
1908                    (Some(a), Some(b)) => Some(TypeExpr::Union(vec![a.clone(), b.clone()])),
1909                    (Some(_), None) => tt,
1910                    (None, Some(_)) => ft,
1911                    (None, None) => None,
1912                }
1913            }
1914
1915            Node::EnumConstruct { enum_name, .. } => Some(TypeExpr::Named(enum_name.clone())),
1916
1917            Node::PropertyAccess { object, property } => {
1918                // EnumName.Variant → infer as the enum type
1919                if let Node::Identifier(name) = &object.node {
1920                    if scope.get_enum(name).is_some() {
1921                        return Some(TypeExpr::Named(name.clone()));
1922                    }
1923                }
1924                // .variant on an enum value → string
1925                if property == "variant" {
1926                    let obj_type = self.infer_type(object, scope);
1927                    if let Some(TypeExpr::Named(name)) = &obj_type {
1928                        if scope.get_enum(name).is_some() {
1929                            return Some(TypeExpr::Named("string".into()));
1930                        }
1931                    }
1932                }
1933                // Shape field access: obj.field → field type
1934                let obj_type = self.infer_type(object, scope);
1935                if let Some(TypeExpr::Shape(fields)) = &obj_type {
1936                    if let Some(field) = fields.iter().find(|f| f.name == *property) {
1937                        return Some(field.type_expr.clone());
1938                    }
1939                }
1940                None
1941            }
1942
1943            Node::SubscriptAccess { object, index } => {
1944                let obj_type = self.infer_type(object, scope);
1945                match &obj_type {
1946                    Some(TypeExpr::List(inner)) => Some(*inner.clone()),
1947                    Some(TypeExpr::DictType(_, v)) => Some(*v.clone()),
1948                    Some(TypeExpr::Shape(fields)) => {
1949                        // If index is a string literal, look up the field type
1950                        if let Node::StringLiteral(key) = &index.node {
1951                            fields
1952                                .iter()
1953                                .find(|f| &f.name == key)
1954                                .map(|f| f.type_expr.clone())
1955                        } else {
1956                            None
1957                        }
1958                    }
1959                    Some(TypeExpr::Named(n)) if n == "list" => None,
1960                    Some(TypeExpr::Named(n)) if n == "dict" => None,
1961                    Some(TypeExpr::Named(n)) if n == "string" => {
1962                        Some(TypeExpr::Named("string".into()))
1963                    }
1964                    _ => None,
1965                }
1966            }
1967            Node::SliceAccess { object, .. } => {
1968                // Slicing a list returns the same list type; slicing a string returns string
1969                let obj_type = self.infer_type(object, scope);
1970                match &obj_type {
1971                    Some(TypeExpr::List(_)) => obj_type,
1972                    Some(TypeExpr::Named(n)) if n == "list" => obj_type,
1973                    Some(TypeExpr::Named(n)) if n == "string" => {
1974                        Some(TypeExpr::Named("string".into()))
1975                    }
1976                    _ => None,
1977                }
1978            }
1979            Node::MethodCall { object, method, .. }
1980            | Node::OptionalMethodCall { object, method, .. } => {
1981                let obj_type = self.infer_type(object, scope);
1982                let is_dict = matches!(&obj_type, Some(TypeExpr::Named(n)) if n == "dict")
1983                    || matches!(&obj_type, Some(TypeExpr::DictType(..)))
1984                    || matches!(&obj_type, Some(TypeExpr::Shape(_)));
1985                match method.as_str() {
1986                    // Shared: bool-returning methods
1987                    "contains" | "starts_with" | "ends_with" | "empty" | "has" | "any" | "all" => {
1988                        Some(TypeExpr::Named("bool".into()))
1989                    }
1990                    // Shared: int-returning methods
1991                    "count" | "index_of" => Some(TypeExpr::Named("int".into())),
1992                    // String methods
1993                    "trim" | "lowercase" | "uppercase" | "reverse" | "replace" | "substring"
1994                    | "pad_left" | "pad_right" | "repeat" | "join" => {
1995                        Some(TypeExpr::Named("string".into()))
1996                    }
1997                    "split" | "chars" => Some(TypeExpr::Named("list".into())),
1998                    // filter returns dict for dicts, list for lists
1999                    "filter" => {
2000                        if is_dict {
2001                            Some(TypeExpr::Named("dict".into()))
2002                        } else {
2003                            Some(TypeExpr::Named("list".into()))
2004                        }
2005                    }
2006                    // List methods
2007                    "map" | "flat_map" | "sort" => Some(TypeExpr::Named("list".into())),
2008                    "reduce" | "find" | "first" | "last" => None,
2009                    // Dict methods
2010                    "keys" | "values" | "entries" => Some(TypeExpr::Named("list".into())),
2011                    "merge" | "map_values" | "rekey" | "map_keys" => {
2012                        // Rekey/map_keys transform keys; resulting dict still keys-by-string.
2013                        // Preserve the value-type parameter when known so downstream code can
2014                        // still rely on dict<string, V> typing after a key-rename.
2015                        if let Some(TypeExpr::DictType(_, v)) = &obj_type {
2016                            Some(TypeExpr::DictType(
2017                                Box::new(TypeExpr::Named("string".into())),
2018                                v.clone(),
2019                            ))
2020                        } else {
2021                            Some(TypeExpr::Named("dict".into()))
2022                        }
2023                    }
2024                    // Conversions
2025                    "to_string" => Some(TypeExpr::Named("string".into())),
2026                    "to_int" => Some(TypeExpr::Named("int".into())),
2027                    "to_float" => Some(TypeExpr::Named("float".into())),
2028                    _ => None,
2029                }
2030            }
2031
2032            // TryOperator on Result<T, E> produces T
2033            Node::TryOperator { operand } => {
2034                match self.infer_type(operand, scope) {
2035                    Some(TypeExpr::Named(name)) if name == "Result" => None, // unknown inner type
2036                    _ => None,
2037                }
2038            }
2039
2040            _ => None,
2041        }
2042    }
2043
2044    /// Check if two types are compatible (actual can be assigned to expected).
2045    fn types_compatible(&self, expected: &TypeExpr, actual: &TypeExpr, scope: &TypeScope) -> bool {
2046        // Generic type parameters match anything.
2047        if let TypeExpr::Named(name) = expected {
2048            if scope.is_generic_type_param(name) {
2049                return true;
2050            }
2051        }
2052        if let TypeExpr::Named(name) = actual {
2053            if scope.is_generic_type_param(name) {
2054                return true;
2055            }
2056        }
2057        let expected = self.resolve_alias(expected, scope);
2058        let actual = self.resolve_alias(actual, scope);
2059
2060        // Interface satisfaction: if expected is an interface name, check if actual type
2061        // has all required methods (Go-style implicit satisfaction).
2062        if let TypeExpr::Named(iface_name) = &expected {
2063            if scope.get_interface(iface_name).is_some() {
2064                if let TypeExpr::Named(type_name) = &actual {
2065                    return self.satisfies_interface(type_name, iface_name, scope);
2066                }
2067                return false;
2068            }
2069        }
2070
2071        match (&expected, &actual) {
2072            (TypeExpr::Named(a), TypeExpr::Named(b)) => a == b || (a == "float" && b == "int"),
2073            // Union-to-Union: every member of actual must be compatible with
2074            // at least one member of expected.
2075            (TypeExpr::Union(exp_members), TypeExpr::Union(act_members)) => {
2076                act_members.iter().all(|am| {
2077                    exp_members
2078                        .iter()
2079                        .any(|em| self.types_compatible(em, am, scope))
2080                })
2081            }
2082            (TypeExpr::Union(members), actual_type) => members
2083                .iter()
2084                .any(|m| self.types_compatible(m, actual_type, scope)),
2085            (expected_type, TypeExpr::Union(members)) => members
2086                .iter()
2087                .all(|m| self.types_compatible(expected_type, m, scope)),
2088            (TypeExpr::Shape(_), TypeExpr::Named(n)) if n == "dict" => true,
2089            (TypeExpr::Named(n), TypeExpr::Shape(_)) if n == "dict" => true,
2090            (TypeExpr::Shape(ef), TypeExpr::Shape(af)) => ef.iter().all(|expected_field| {
2091                if expected_field.optional {
2092                    return true;
2093                }
2094                af.iter().any(|actual_field| {
2095                    actual_field.name == expected_field.name
2096                        && self.types_compatible(
2097                            &expected_field.type_expr,
2098                            &actual_field.type_expr,
2099                            scope,
2100                        )
2101                })
2102            }),
2103            // dict<K, V> expected, Shape actual → all field values must match V
2104            (TypeExpr::DictType(ek, ev), TypeExpr::Shape(af)) => {
2105                let keys_ok = matches!(ek.as_ref(), TypeExpr::Named(n) if n == "string");
2106                keys_ok
2107                    && af
2108                        .iter()
2109                        .all(|f| self.types_compatible(ev, &f.type_expr, scope))
2110            }
2111            // Shape expected, dict<K, V> actual → gradual: allow since dict may have the fields
2112            (TypeExpr::Shape(_), TypeExpr::DictType(_, _)) => true,
2113            (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
2114                self.types_compatible(expected_inner, actual_inner, scope)
2115            }
2116            (TypeExpr::Named(n), TypeExpr::List(_)) if n == "list" => true,
2117            (TypeExpr::List(_), TypeExpr::Named(n)) if n == "list" => true,
2118            (TypeExpr::DictType(ek, ev), TypeExpr::DictType(ak, av)) => {
2119                self.types_compatible(ek, ak, scope) && self.types_compatible(ev, av, scope)
2120            }
2121            (TypeExpr::Named(n), TypeExpr::DictType(_, _)) if n == "dict" => true,
2122            (TypeExpr::DictType(_, _), TypeExpr::Named(n)) if n == "dict" => true,
2123            // FnType compatibility: params match positionally and return types match
2124            (
2125                TypeExpr::FnType {
2126                    params: ep,
2127                    return_type: er,
2128                },
2129                TypeExpr::FnType {
2130                    params: ap,
2131                    return_type: ar,
2132                },
2133            ) => {
2134                ep.len() == ap.len()
2135                    && ep
2136                        .iter()
2137                        .zip(ap.iter())
2138                        .all(|(e, a)| self.types_compatible(e, a, scope))
2139                    && self.types_compatible(er, ar, scope)
2140            }
2141            // FnType is compatible with Named("closure") for backward compat
2142            (TypeExpr::FnType { .. }, TypeExpr::Named(n)) if n == "closure" => true,
2143            (TypeExpr::Named(n), TypeExpr::FnType { .. }) if n == "closure" => true,
2144            _ => false,
2145        }
2146    }
2147
2148    fn resolve_alias<'a>(&self, ty: &'a TypeExpr, scope: &'a TypeScope) -> TypeExpr {
2149        if let TypeExpr::Named(name) = ty {
2150            if let Some(resolved) = scope.resolve_type(name) {
2151                return resolved.clone();
2152            }
2153        }
2154        ty.clone()
2155    }
2156
2157    fn error_at(&mut self, message: String, span: Span) {
2158        self.diagnostics.push(TypeDiagnostic {
2159            message,
2160            severity: DiagnosticSeverity::Error,
2161            span: Some(span),
2162            help: None,
2163        });
2164    }
2165
2166    #[allow(dead_code)]
2167    fn error_at_with_help(&mut self, message: String, span: Span, help: String) {
2168        self.diagnostics.push(TypeDiagnostic {
2169            message,
2170            severity: DiagnosticSeverity::Error,
2171            span: Some(span),
2172            help: Some(help),
2173        });
2174    }
2175
2176    fn warning_at(&mut self, message: String, span: Span) {
2177        self.diagnostics.push(TypeDiagnostic {
2178            message,
2179            severity: DiagnosticSeverity::Warning,
2180            span: Some(span),
2181            help: None,
2182        });
2183    }
2184
2185    #[allow(dead_code)]
2186    fn warning_at_with_help(&mut self, message: String, span: Span, help: String) {
2187        self.diagnostics.push(TypeDiagnostic {
2188            message,
2189            severity: DiagnosticSeverity::Warning,
2190            span: Some(span),
2191            help: Some(help),
2192        });
2193    }
2194}
2195
2196impl Default for TypeChecker {
2197    fn default() -> Self {
2198        Self::new()
2199    }
2200}
2201
2202/// Infer the result type of a binary operation.
2203fn infer_binary_op_type(op: &str, left: &InferredType, right: &InferredType) -> InferredType {
2204    match op {
2205        "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
2206            Some(TypeExpr::Named("bool".into()))
2207        }
2208        "+" => match (left, right) {
2209            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2210                match (l.as_str(), r.as_str()) {
2211                    ("int", "int") => Some(TypeExpr::Named("int".into())),
2212                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2213                    ("string", "string") => Some(TypeExpr::Named("string".into())),
2214                    ("list", "list") => Some(TypeExpr::Named("list".into())),
2215                    ("dict", "dict") => Some(TypeExpr::Named("dict".into())),
2216                    _ => None,
2217                }
2218            }
2219            _ => None,
2220        },
2221        "-" | "/" | "%" => match (left, right) {
2222            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2223                match (l.as_str(), r.as_str()) {
2224                    ("int", "int") => Some(TypeExpr::Named("int".into())),
2225                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2226                    _ => None,
2227                }
2228            }
2229            _ => None,
2230        },
2231        "*" => match (left, right) {
2232            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2233                match (l.as_str(), r.as_str()) {
2234                    ("string", "int") | ("int", "string") => Some(TypeExpr::Named("string".into())),
2235                    ("int", "int") => Some(TypeExpr::Named("int".into())),
2236                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2237                    _ => None,
2238                }
2239            }
2240            _ => None,
2241        },
2242        "??" => match (left, right) {
2243            (Some(TypeExpr::Union(members)), _) => {
2244                let non_nil: Vec<_> = members
2245                    .iter()
2246                    .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
2247                    .cloned()
2248                    .collect();
2249                if non_nil.len() == 1 {
2250                    Some(non_nil[0].clone())
2251                } else if non_nil.is_empty() {
2252                    right.clone()
2253                } else {
2254                    Some(TypeExpr::Union(non_nil))
2255                }
2256            }
2257            _ => right.clone(),
2258        },
2259        "|>" => None,
2260        _ => None,
2261    }
2262}
2263
2264/// Format a type expression for display in error messages.
2265/// Produce a detail string describing why a Shape type is incompatible with
2266/// another Shape type — e.g. "missing field 'age' (int)" or "field 'name'
2267/// has type int, expected string".  Returns `None` if both types are not shapes.
2268pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
2269    if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
2270        let mut details = Vec::new();
2271        for field in ef {
2272            if field.optional {
2273                continue;
2274            }
2275            match af.iter().find(|f| f.name == field.name) {
2276                None => details.push(format!(
2277                    "missing field '{}' ({})",
2278                    field.name,
2279                    format_type(&field.type_expr)
2280                )),
2281                Some(actual_field) => {
2282                    let e_str = format_type(&field.type_expr);
2283                    let a_str = format_type(&actual_field.type_expr);
2284                    if e_str != a_str {
2285                        details.push(format!(
2286                            "field '{}' has type {}, expected {}",
2287                            field.name, a_str, e_str
2288                        ));
2289                    }
2290                }
2291            }
2292        }
2293        if details.is_empty() {
2294            None
2295        } else {
2296            Some(details.join("; "))
2297        }
2298    } else {
2299        None
2300    }
2301}
2302
2303pub fn format_type(ty: &TypeExpr) -> String {
2304    match ty {
2305        TypeExpr::Named(n) => n.clone(),
2306        TypeExpr::Union(types) => types
2307            .iter()
2308            .map(format_type)
2309            .collect::<Vec<_>>()
2310            .join(" | "),
2311        TypeExpr::Shape(fields) => {
2312            let inner: Vec<String> = fields
2313                .iter()
2314                .map(|f| {
2315                    let opt = if f.optional { "?" } else { "" };
2316                    format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
2317                })
2318                .collect();
2319            format!("{{{}}}", inner.join(", "))
2320        }
2321        TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
2322        TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
2323        TypeExpr::FnType {
2324            params,
2325            return_type,
2326        } => {
2327            let params_str = params
2328                .iter()
2329                .map(format_type)
2330                .collect::<Vec<_>>()
2331                .join(", ");
2332            format!("fn({}) -> {}", params_str, format_type(return_type))
2333        }
2334    }
2335}
2336
2337/// Remove a named type from a union, collapsing single-element unions.
2338fn remove_from_union(members: &[TypeExpr], to_remove: &str) -> InferredType {
2339    let remaining: Vec<TypeExpr> = members
2340        .iter()
2341        .filter(|m| !matches!(m, TypeExpr::Named(n) if n == to_remove))
2342        .cloned()
2343        .collect();
2344    match remaining.len() {
2345        0 => None,
2346        1 => Some(remaining.into_iter().next().unwrap()),
2347        _ => Some(TypeExpr::Union(remaining)),
2348    }
2349}
2350
2351/// Narrow a union to just one named type, if that type is a member.
2352fn narrow_to_single(members: &[TypeExpr], target: &str) -> InferredType {
2353    if members
2354        .iter()
2355        .any(|m| matches!(m, TypeExpr::Named(n) if n == target))
2356    {
2357        Some(TypeExpr::Named(target.to_string()))
2358    } else {
2359        None
2360    }
2361}
2362
2363/// Extract the variable name from a `type_of(x)` call.
2364fn extract_type_of_var(node: &SNode) -> Option<String> {
2365    if let Node::FunctionCall { name, args } = &node.node {
2366        if name == "type_of" && args.len() == 1 {
2367            if let Node::Identifier(var) = &args[0].node {
2368                return Some(var.clone());
2369            }
2370        }
2371    }
2372    None
2373}
2374
2375/// Apply a list of refinements to a scope, tracking pre-narrowing types.
2376fn apply_refinements(scope: &mut TypeScope, refinements: &[(String, InferredType)]) {
2377    for (var_name, narrowed_type) in refinements {
2378        // Save the pre-narrowing type so we can restore it on reassignment
2379        if !scope.narrowed_vars.contains_key(var_name) {
2380            if let Some(original) = scope.get_var(var_name).cloned() {
2381                scope.narrowed_vars.insert(var_name.clone(), original);
2382            }
2383        }
2384        scope.define_var(var_name, narrowed_type.clone());
2385    }
2386}
2387
2388#[cfg(test)]
2389mod tests {
2390    use super::*;
2391    use crate::Parser;
2392    use harn_lexer::Lexer;
2393
2394    fn check_source(source: &str) -> Vec<TypeDiagnostic> {
2395        let mut lexer = Lexer::new(source);
2396        let tokens = lexer.tokenize().unwrap();
2397        let mut parser = Parser::new(tokens);
2398        let program = parser.parse().unwrap();
2399        TypeChecker::new().check(&program)
2400    }
2401
2402    fn errors(source: &str) -> Vec<String> {
2403        check_source(source)
2404            .into_iter()
2405            .filter(|d| d.severity == DiagnosticSeverity::Error)
2406            .map(|d| d.message)
2407            .collect()
2408    }
2409
2410    #[test]
2411    fn test_no_errors_for_untyped_code() {
2412        let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
2413        assert!(errs.is_empty());
2414    }
2415
2416    #[test]
2417    fn test_correct_typed_let() {
2418        let errs = errors("pipeline t(task) { let x: int = 42 }");
2419        assert!(errs.is_empty());
2420    }
2421
2422    #[test]
2423    fn test_type_mismatch_let() {
2424        let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
2425        assert_eq!(errs.len(), 1);
2426        assert!(errs[0].contains("Type mismatch"));
2427        assert!(errs[0].contains("int"));
2428        assert!(errs[0].contains("string"));
2429    }
2430
2431    #[test]
2432    fn test_correct_typed_fn() {
2433        let errs = errors(
2434            "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
2435        );
2436        assert!(errs.is_empty());
2437    }
2438
2439    #[test]
2440    fn test_fn_arg_type_mismatch() {
2441        let errs = errors(
2442            r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
2443add("hello", 2) }"#,
2444        );
2445        assert_eq!(errs.len(), 1);
2446        assert!(errs[0].contains("Argument 1"));
2447        assert!(errs[0].contains("expected int"));
2448    }
2449
2450    #[test]
2451    fn test_return_type_mismatch() {
2452        let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
2453        assert_eq!(errs.len(), 1);
2454        assert!(errs[0].contains("Return type mismatch"));
2455    }
2456
2457    #[test]
2458    fn test_union_type_compatible() {
2459        let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
2460        assert!(errs.is_empty());
2461    }
2462
2463    #[test]
2464    fn test_union_type_mismatch() {
2465        let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
2466        assert_eq!(errs.len(), 1);
2467        assert!(errs[0].contains("Type mismatch"));
2468    }
2469
2470    #[test]
2471    fn test_type_inference_propagation() {
2472        let errs = errors(
2473            r#"pipeline t(task) {
2474  fn add(a: int, b: int) -> int { return a + b }
2475  let result: string = add(1, 2)
2476}"#,
2477        );
2478        assert_eq!(errs.len(), 1);
2479        assert!(errs[0].contains("Type mismatch"));
2480        assert!(errs[0].contains("string"));
2481        assert!(errs[0].contains("int"));
2482    }
2483
2484    #[test]
2485    fn test_builtin_return_type_inference() {
2486        let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
2487        assert_eq!(errs.len(), 1);
2488        assert!(errs[0].contains("string"));
2489        assert!(errs[0].contains("int"));
2490    }
2491
2492    #[test]
2493    fn test_workflow_and_transcript_builtins_are_known() {
2494        let errs = errors(
2495            r#"pipeline t(task) {
2496  let flow = workflow_graph({name: "demo", entry: "act", nodes: {act: {kind: "stage"}}})
2497  let report: dict = workflow_policy_report(flow, {tools: tool_registry(), capabilities: {workspace: ["read_text"]}})
2498  let run: dict = workflow_execute("task", flow, [], {})
2499  let tree: dict = load_run_tree("run.json")
2500  let fixture: dict = run_record_fixture(run?.run)
2501  let suite: dict = run_record_eval_suite([{run: run?.run, fixture: fixture}])
2502  let diff: dict = run_record_diff(run?.run, run?.run)
2503  let manifest: dict = eval_suite_manifest({cases: [{run_path: "run.json"}]})
2504  let suite_report: dict = eval_suite_run(manifest)
2505  let wf: dict = artifact_workspace_file("src/main.rs", "fn main() {}", {source: "host"})
2506  let snap: dict = artifact_workspace_snapshot(["src/main.rs"], "snapshot")
2507  let selection: dict = artifact_editor_selection("src/main.rs", "main")
2508  let verify: dict = artifact_verification_result("verify", "ok")
2509  let test_result: dict = artifact_test_result("tests", "pass")
2510  let cmd: dict = artifact_command_result("cargo test", {status: 0})
2511  let patch: dict = artifact_diff("src/main.rs", "old", "new")
2512  let git: dict = artifact_git_diff("diff --git a b")
2513  let review: dict = artifact_diff_review(patch, "review me")
2514  let decision: dict = artifact_review_decision(review, "accepted")
2515  let proposal: dict = artifact_patch_proposal(review, "*** Begin Patch")
2516  let bundle: dict = artifact_verification_bundle("checks", [{name: "fmt", ok: true}])
2517  let apply: dict = artifact_apply_intent(review, "apply")
2518  let transcript = transcript_reset({metadata: {source: "test"}})
2519  let visible: string = transcript_render_visible(transcript_archive(transcript))
2520  let events: list = transcript_events(transcript)
2521  let context: string = artifact_context([], {max_artifacts: 1})
2522  println(report)
2523  println(run)
2524  println(tree)
2525  println(fixture)
2526  println(suite)
2527  println(diff)
2528  println(manifest)
2529  println(suite_report)
2530  println(wf)
2531  println(snap)
2532  println(selection)
2533  println(verify)
2534  println(test_result)
2535  println(cmd)
2536  println(patch)
2537  println(git)
2538  println(review)
2539  println(decision)
2540  println(proposal)
2541  println(bundle)
2542  println(apply)
2543  println(visible)
2544  println(events)
2545  println(context)
2546}"#,
2547        );
2548        assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
2549    }
2550
2551    #[test]
2552    fn test_binary_op_type_inference() {
2553        let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
2554        assert_eq!(errs.len(), 1);
2555    }
2556
2557    #[test]
2558    fn test_comparison_returns_bool() {
2559        let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
2560        assert!(errs.is_empty());
2561    }
2562
2563    #[test]
2564    fn test_int_float_promotion() {
2565        let errs = errors("pipeline t(task) { let x: float = 42 }");
2566        assert!(errs.is_empty());
2567    }
2568
2569    #[test]
2570    fn test_untyped_code_no_errors() {
2571        let errs = errors(
2572            r#"pipeline t(task) {
2573  fn process(data) {
2574    let result = data + " processed"
2575    return result
2576  }
2577  log(process("hello"))
2578}"#,
2579        );
2580        assert!(errs.is_empty());
2581    }
2582
2583    #[test]
2584    fn test_type_alias() {
2585        let errs = errors(
2586            r#"pipeline t(task) {
2587  type Name = string
2588  let x: Name = "hello"
2589}"#,
2590        );
2591        assert!(errs.is_empty());
2592    }
2593
2594    #[test]
2595    fn test_type_alias_mismatch() {
2596        let errs = errors(
2597            r#"pipeline t(task) {
2598  type Name = string
2599  let x: Name = 42
2600}"#,
2601        );
2602        assert_eq!(errs.len(), 1);
2603    }
2604
2605    #[test]
2606    fn test_assignment_type_check() {
2607        let errs = errors(
2608            r#"pipeline t(task) {
2609  var x: int = 0
2610  x = "hello"
2611}"#,
2612        );
2613        assert_eq!(errs.len(), 1);
2614        assert!(errs[0].contains("cannot assign string"));
2615    }
2616
2617    #[test]
2618    fn test_covariance_int_to_float_in_fn() {
2619        let errs = errors(
2620            "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
2621        );
2622        assert!(errs.is_empty());
2623    }
2624
2625    #[test]
2626    fn test_covariance_return_type() {
2627        let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
2628        assert!(errs.is_empty());
2629    }
2630
2631    #[test]
2632    fn test_no_contravariance_float_to_int() {
2633        let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
2634        assert_eq!(errs.len(), 1);
2635    }
2636
2637    // --- Exhaustiveness checking tests ---
2638
2639    fn warnings(source: &str) -> Vec<String> {
2640        check_source(source)
2641            .into_iter()
2642            .filter(|d| d.severity == DiagnosticSeverity::Warning)
2643            .map(|d| d.message)
2644            .collect()
2645    }
2646
2647    #[test]
2648    fn test_exhaustive_match_no_warning() {
2649        let warns = warnings(
2650            r#"pipeline t(task) {
2651  enum Color { Red, Green, Blue }
2652  let c = Color.Red
2653  match c.variant {
2654    "Red" -> { log("r") }
2655    "Green" -> { log("g") }
2656    "Blue" -> { log("b") }
2657  }
2658}"#,
2659        );
2660        let exhaustive_warns: Vec<_> = warns
2661            .iter()
2662            .filter(|w| w.contains("Non-exhaustive"))
2663            .collect();
2664        assert!(exhaustive_warns.is_empty());
2665    }
2666
2667    #[test]
2668    fn test_non_exhaustive_match_warning() {
2669        let warns = warnings(
2670            r#"pipeline t(task) {
2671  enum Color { Red, Green, Blue }
2672  let c = Color.Red
2673  match c.variant {
2674    "Red" -> { log("r") }
2675    "Green" -> { log("g") }
2676  }
2677}"#,
2678        );
2679        let exhaustive_warns: Vec<_> = warns
2680            .iter()
2681            .filter(|w| w.contains("Non-exhaustive"))
2682            .collect();
2683        assert_eq!(exhaustive_warns.len(), 1);
2684        assert!(exhaustive_warns[0].contains("Blue"));
2685    }
2686
2687    #[test]
2688    fn test_non_exhaustive_multiple_missing() {
2689        let warns = warnings(
2690            r#"pipeline t(task) {
2691  enum Status { Active, Inactive, Pending }
2692  let s = Status.Active
2693  match s.variant {
2694    "Active" -> { log("a") }
2695  }
2696}"#,
2697        );
2698        let exhaustive_warns: Vec<_> = warns
2699            .iter()
2700            .filter(|w| w.contains("Non-exhaustive"))
2701            .collect();
2702        assert_eq!(exhaustive_warns.len(), 1);
2703        assert!(exhaustive_warns[0].contains("Inactive"));
2704        assert!(exhaustive_warns[0].contains("Pending"));
2705    }
2706
2707    #[test]
2708    fn test_enum_construct_type_inference() {
2709        let errs = errors(
2710            r#"pipeline t(task) {
2711  enum Color { Red, Green, Blue }
2712  let c: Color = Color.Red
2713}"#,
2714        );
2715        assert!(errs.is_empty());
2716    }
2717
2718    // --- Type narrowing tests ---
2719
2720    #[test]
2721    fn test_nil_coalescing_strips_nil() {
2722        // After ??, nil should be stripped from the type
2723        let errs = errors(
2724            r#"pipeline t(task) {
2725  let x: string | nil = nil
2726  let y: string = x ?? "default"
2727}"#,
2728        );
2729        assert!(errs.is_empty());
2730    }
2731
2732    #[test]
2733    fn test_shape_mismatch_detail_missing_field() {
2734        let errs = errors(
2735            r#"pipeline t(task) {
2736  let x: {name: string, age: int} = {name: "hello"}
2737}"#,
2738        );
2739        assert_eq!(errs.len(), 1);
2740        assert!(
2741            errs[0].contains("missing field 'age'"),
2742            "expected detail about missing field, got: {}",
2743            errs[0]
2744        );
2745    }
2746
2747    #[test]
2748    fn test_shape_mismatch_detail_wrong_type() {
2749        let errs = errors(
2750            r#"pipeline t(task) {
2751  let x: {name: string, age: int} = {name: 42, age: 10}
2752}"#,
2753        );
2754        assert_eq!(errs.len(), 1);
2755        assert!(
2756            errs[0].contains("field 'name' has type int, expected string"),
2757            "expected detail about wrong type, got: {}",
2758            errs[0]
2759        );
2760    }
2761
2762    // --- Match pattern type validation tests ---
2763
2764    #[test]
2765    fn test_match_pattern_string_against_int() {
2766        let warns = warnings(
2767            r#"pipeline t(task) {
2768  let x: int = 42
2769  match x {
2770    "hello" -> { log("bad") }
2771    42 -> { log("ok") }
2772  }
2773}"#,
2774        );
2775        let pattern_warns: Vec<_> = warns
2776            .iter()
2777            .filter(|w| w.contains("Match pattern type mismatch"))
2778            .collect();
2779        assert_eq!(pattern_warns.len(), 1);
2780        assert!(pattern_warns[0].contains("matching int against string literal"));
2781    }
2782
2783    #[test]
2784    fn test_match_pattern_int_against_string() {
2785        let warns = warnings(
2786            r#"pipeline t(task) {
2787  let x: string = "hello"
2788  match x {
2789    42 -> { log("bad") }
2790    "hello" -> { log("ok") }
2791  }
2792}"#,
2793        );
2794        let pattern_warns: Vec<_> = warns
2795            .iter()
2796            .filter(|w| w.contains("Match pattern type mismatch"))
2797            .collect();
2798        assert_eq!(pattern_warns.len(), 1);
2799        assert!(pattern_warns[0].contains("matching string against int literal"));
2800    }
2801
2802    #[test]
2803    fn test_match_pattern_bool_against_int() {
2804        let warns = warnings(
2805            r#"pipeline t(task) {
2806  let x: int = 42
2807  match x {
2808    true -> { log("bad") }
2809    42 -> { log("ok") }
2810  }
2811}"#,
2812        );
2813        let pattern_warns: Vec<_> = warns
2814            .iter()
2815            .filter(|w| w.contains("Match pattern type mismatch"))
2816            .collect();
2817        assert_eq!(pattern_warns.len(), 1);
2818        assert!(pattern_warns[0].contains("matching int against bool literal"));
2819    }
2820
2821    #[test]
2822    fn test_match_pattern_float_against_string() {
2823        let warns = warnings(
2824            r#"pipeline t(task) {
2825  let x: string = "hello"
2826  match x {
2827    3.14 -> { log("bad") }
2828    "hello" -> { log("ok") }
2829  }
2830}"#,
2831        );
2832        let pattern_warns: Vec<_> = warns
2833            .iter()
2834            .filter(|w| w.contains("Match pattern type mismatch"))
2835            .collect();
2836        assert_eq!(pattern_warns.len(), 1);
2837        assert!(pattern_warns[0].contains("matching string against float literal"));
2838    }
2839
2840    #[test]
2841    fn test_match_pattern_int_against_float_ok() {
2842        // int and float are compatible for match patterns
2843        let warns = warnings(
2844            r#"pipeline t(task) {
2845  let x: float = 3.14
2846  match x {
2847    42 -> { log("ok") }
2848    _ -> { log("default") }
2849  }
2850}"#,
2851        );
2852        let pattern_warns: Vec<_> = warns
2853            .iter()
2854            .filter(|w| w.contains("Match pattern type mismatch"))
2855            .collect();
2856        assert!(pattern_warns.is_empty());
2857    }
2858
2859    #[test]
2860    fn test_match_pattern_float_against_int_ok() {
2861        // float and int are compatible for match patterns
2862        let warns = warnings(
2863            r#"pipeline t(task) {
2864  let x: int = 42
2865  match x {
2866    3.14 -> { log("close") }
2867    _ -> { log("default") }
2868  }
2869}"#,
2870        );
2871        let pattern_warns: Vec<_> = warns
2872            .iter()
2873            .filter(|w| w.contains("Match pattern type mismatch"))
2874            .collect();
2875        assert!(pattern_warns.is_empty());
2876    }
2877
2878    #[test]
2879    fn test_match_pattern_correct_types_no_warning() {
2880        let warns = warnings(
2881            r#"pipeline t(task) {
2882  let x: int = 42
2883  match x {
2884    1 -> { log("one") }
2885    2 -> { log("two") }
2886    _ -> { log("other") }
2887  }
2888}"#,
2889        );
2890        let pattern_warns: Vec<_> = warns
2891            .iter()
2892            .filter(|w| w.contains("Match pattern type mismatch"))
2893            .collect();
2894        assert!(pattern_warns.is_empty());
2895    }
2896
2897    #[test]
2898    fn test_match_pattern_wildcard_no_warning() {
2899        let warns = warnings(
2900            r#"pipeline t(task) {
2901  let x: int = 42
2902  match x {
2903    _ -> { log("catch all") }
2904  }
2905}"#,
2906        );
2907        let pattern_warns: Vec<_> = warns
2908            .iter()
2909            .filter(|w| w.contains("Match pattern type mismatch"))
2910            .collect();
2911        assert!(pattern_warns.is_empty());
2912    }
2913
2914    #[test]
2915    fn test_match_pattern_untyped_no_warning() {
2916        // When value has no known type, no warning should be emitted
2917        let warns = warnings(
2918            r#"pipeline t(task) {
2919  let x = some_unknown_fn()
2920  match x {
2921    "hello" -> { log("string") }
2922    42 -> { log("int") }
2923  }
2924}"#,
2925        );
2926        let pattern_warns: Vec<_> = warns
2927            .iter()
2928            .filter(|w| w.contains("Match pattern type mismatch"))
2929            .collect();
2930        assert!(pattern_warns.is_empty());
2931    }
2932
2933    // --- Interface constraint type checking tests ---
2934
2935    fn iface_warns(source: &str) -> Vec<String> {
2936        warnings(source)
2937            .into_iter()
2938            .filter(|w| w.contains("does not satisfy interface"))
2939            .collect()
2940    }
2941
2942    #[test]
2943    fn test_interface_constraint_return_type_mismatch() {
2944        let warns = iface_warns(
2945            r#"pipeline t(task) {
2946  interface Sizable {
2947    fn size(self) -> int
2948  }
2949  struct Box { width: int }
2950  impl Box {
2951    fn size(self) -> string { return "nope" }
2952  }
2953  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2954  measure(Box({width: 3}))
2955}"#,
2956        );
2957        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2958        assert!(
2959            warns[0].contains("method 'size' returns 'string', expected 'int'"),
2960            "unexpected message: {}",
2961            warns[0]
2962        );
2963    }
2964
2965    #[test]
2966    fn test_interface_constraint_param_type_mismatch() {
2967        let warns = iface_warns(
2968            r#"pipeline t(task) {
2969  interface Processor {
2970    fn process(self, x: int) -> string
2971  }
2972  struct MyProc { name: string }
2973  impl MyProc {
2974    fn process(self, x: string) -> string { return x }
2975  }
2976  fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
2977  run_proc(MyProc({name: "a"}))
2978}"#,
2979        );
2980        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2981        assert!(
2982            warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
2983            "unexpected message: {}",
2984            warns[0]
2985        );
2986    }
2987
2988    #[test]
2989    fn test_interface_constraint_missing_method() {
2990        let warns = iface_warns(
2991            r#"pipeline t(task) {
2992  interface Sizable {
2993    fn size(self) -> int
2994  }
2995  struct Box { width: int }
2996  impl Box {
2997    fn area(self) -> int { return self.width }
2998  }
2999  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3000  measure(Box({width: 3}))
3001}"#,
3002        );
3003        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3004        assert!(
3005            warns[0].contains("missing method 'size'"),
3006            "unexpected message: {}",
3007            warns[0]
3008        );
3009    }
3010
3011    #[test]
3012    fn test_interface_constraint_param_count_mismatch() {
3013        let warns = iface_warns(
3014            r#"pipeline t(task) {
3015  interface Doubler {
3016    fn double(self, x: int) -> int
3017  }
3018  struct Bad { v: int }
3019  impl Bad {
3020    fn double(self) -> int { return self.v * 2 }
3021  }
3022  fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
3023  run_double(Bad({v: 5}))
3024}"#,
3025        );
3026        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3027        assert!(
3028            warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
3029            "unexpected message: {}",
3030            warns[0]
3031        );
3032    }
3033
3034    #[test]
3035    fn test_interface_constraint_satisfied() {
3036        let warns = iface_warns(
3037            r#"pipeline t(task) {
3038  interface Sizable {
3039    fn size(self) -> int
3040  }
3041  struct Box { width: int, height: int }
3042  impl Box {
3043    fn size(self) -> int { return self.width * self.height }
3044  }
3045  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3046  measure(Box({width: 3, height: 4}))
3047}"#,
3048        );
3049        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3050    }
3051
3052    #[test]
3053    fn test_interface_constraint_untyped_impl_compatible() {
3054        // Gradual typing: untyped impl return should not trigger warning
3055        let warns = iface_warns(
3056            r#"pipeline t(task) {
3057  interface Sizable {
3058    fn size(self) -> int
3059  }
3060  struct Box { width: int }
3061  impl Box {
3062    fn size(self) { return self.width }
3063  }
3064  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3065  measure(Box({width: 3}))
3066}"#,
3067        );
3068        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3069    }
3070
3071    #[test]
3072    fn test_interface_constraint_int_float_covariance() {
3073        // int is compatible with float (covariance)
3074        let warns = iface_warns(
3075            r#"pipeline t(task) {
3076  interface Measurable {
3077    fn value(self) -> float
3078  }
3079  struct Gauge { v: int }
3080  impl Gauge {
3081    fn value(self) -> int { return self.v }
3082  }
3083  fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
3084  read_val(Gauge({v: 42}))
3085}"#,
3086        );
3087        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3088    }
3089
3090    // --- Flow-sensitive type refinement tests ---
3091
3092    #[test]
3093    fn test_nil_narrowing_then_branch() {
3094        // Existing behavior: x != nil narrows to string in then-branch
3095        let errs = errors(
3096            r#"pipeline t(task) {
3097  fn greet(name: string | nil) {
3098    if name != nil {
3099      let s: string = name
3100    }
3101  }
3102}"#,
3103        );
3104        assert!(errs.is_empty(), "got: {:?}", errs);
3105    }
3106
3107    #[test]
3108    fn test_nil_narrowing_else_branch() {
3109        // NEW: x != nil narrows to nil in else-branch
3110        let errs = errors(
3111            r#"pipeline t(task) {
3112  fn check(x: string | nil) {
3113    if x != nil {
3114      let s: string = x
3115    } else {
3116      let n: nil = x
3117    }
3118  }
3119}"#,
3120        );
3121        assert!(errs.is_empty(), "got: {:?}", errs);
3122    }
3123
3124    #[test]
3125    fn test_nil_equality_narrows_both() {
3126        // x == nil narrows then to nil, else to non-nil
3127        let errs = errors(
3128            r#"pipeline t(task) {
3129  fn check(x: string | nil) {
3130    if x == nil {
3131      let n: nil = x
3132    } else {
3133      let s: string = x
3134    }
3135  }
3136}"#,
3137        );
3138        assert!(errs.is_empty(), "got: {:?}", errs);
3139    }
3140
3141    #[test]
3142    fn test_truthiness_narrowing() {
3143        // Bare identifier in condition removes nil
3144        let errs = errors(
3145            r#"pipeline t(task) {
3146  fn check(x: string | nil) {
3147    if x {
3148      let s: string = x
3149    }
3150  }
3151}"#,
3152        );
3153        assert!(errs.is_empty(), "got: {:?}", errs);
3154    }
3155
3156    #[test]
3157    fn test_negation_narrowing() {
3158        // !x swaps truthy/falsy
3159        let errs = errors(
3160            r#"pipeline t(task) {
3161  fn check(x: string | nil) {
3162    if !x {
3163      let n: nil = x
3164    } else {
3165      let s: string = x
3166    }
3167  }
3168}"#,
3169        );
3170        assert!(errs.is_empty(), "got: {:?}", errs);
3171    }
3172
3173    #[test]
3174    fn test_typeof_narrowing() {
3175        // type_of(x) == "string" narrows to string
3176        let errs = errors(
3177            r#"pipeline t(task) {
3178  fn check(x: string | int) {
3179    if type_of(x) == "string" {
3180      let s: string = x
3181    }
3182  }
3183}"#,
3184        );
3185        assert!(errs.is_empty(), "got: {:?}", errs);
3186    }
3187
3188    #[test]
3189    fn test_typeof_narrowing_else() {
3190        // else removes the tested type
3191        let errs = errors(
3192            r#"pipeline t(task) {
3193  fn check(x: string | int) {
3194    if type_of(x) == "string" {
3195      let s: string = x
3196    } else {
3197      let i: int = x
3198    }
3199  }
3200}"#,
3201        );
3202        assert!(errs.is_empty(), "got: {:?}", errs);
3203    }
3204
3205    #[test]
3206    fn test_typeof_neq_narrowing() {
3207        // type_of(x) != "string" removes string in then, narrows to string in else
3208        let errs = errors(
3209            r#"pipeline t(task) {
3210  fn check(x: string | int) {
3211    if type_of(x) != "string" {
3212      let i: int = x
3213    } else {
3214      let s: string = x
3215    }
3216  }
3217}"#,
3218        );
3219        assert!(errs.is_empty(), "got: {:?}", errs);
3220    }
3221
3222    #[test]
3223    fn test_and_combines_narrowing() {
3224        // && combines truthy refinements
3225        let errs = errors(
3226            r#"pipeline t(task) {
3227  fn check(x: string | int | nil) {
3228    if x != nil && type_of(x) == "string" {
3229      let s: string = x
3230    }
3231  }
3232}"#,
3233        );
3234        assert!(errs.is_empty(), "got: {:?}", errs);
3235    }
3236
3237    #[test]
3238    fn test_or_falsy_narrowing() {
3239        // || combines falsy refinements
3240        let errs = errors(
3241            r#"pipeline t(task) {
3242  fn check(x: string | nil, y: int | nil) {
3243    if x || y {
3244      // conservative: can't narrow
3245    } else {
3246      let xn: nil = x
3247      let yn: nil = y
3248    }
3249  }
3250}"#,
3251        );
3252        assert!(errs.is_empty(), "got: {:?}", errs);
3253    }
3254
3255    #[test]
3256    fn test_guard_narrows_outer_scope() {
3257        let errs = errors(
3258            r#"pipeline t(task) {
3259  fn check(x: string | nil) {
3260    guard x != nil else { return }
3261    let s: string = x
3262  }
3263}"#,
3264        );
3265        assert!(errs.is_empty(), "got: {:?}", errs);
3266    }
3267
3268    #[test]
3269    fn test_while_narrows_body() {
3270        let errs = errors(
3271            r#"pipeline t(task) {
3272  fn check(x: string | nil) {
3273    while x != nil {
3274      let s: string = x
3275      break
3276    }
3277  }
3278}"#,
3279        );
3280        assert!(errs.is_empty(), "got: {:?}", errs);
3281    }
3282
3283    #[test]
3284    fn test_early_return_narrows_after_if() {
3285        // if then-body returns, falsy refinements apply after
3286        let errs = errors(
3287            r#"pipeline t(task) {
3288  fn check(x: string | nil) -> string {
3289    if x == nil {
3290      return "default"
3291    }
3292    let s: string = x
3293    return s
3294  }
3295}"#,
3296        );
3297        assert!(errs.is_empty(), "got: {:?}", errs);
3298    }
3299
3300    #[test]
3301    fn test_early_throw_narrows_after_if() {
3302        let errs = errors(
3303            r#"pipeline t(task) {
3304  fn check(x: string | nil) {
3305    if x == nil {
3306      throw "missing"
3307    }
3308    let s: string = x
3309  }
3310}"#,
3311        );
3312        assert!(errs.is_empty(), "got: {:?}", errs);
3313    }
3314
3315    #[test]
3316    fn test_no_narrowing_unknown_type() {
3317        // Gradual typing: untyped vars don't get narrowed
3318        let errs = errors(
3319            r#"pipeline t(task) {
3320  fn check(x) {
3321    if x != nil {
3322      let s: string = x
3323    }
3324  }
3325}"#,
3326        );
3327        // No narrowing possible, so assigning untyped x to string should be fine
3328        // (gradual typing allows it)
3329        assert!(errs.is_empty(), "got: {:?}", errs);
3330    }
3331
3332    #[test]
3333    fn test_reassignment_invalidates_narrowing() {
3334        // After reassigning a narrowed var, the original type should be restored
3335        let errs = errors(
3336            r#"pipeline t(task) {
3337  fn check(x: string | nil) {
3338    var y: string | nil = x
3339    if y != nil {
3340      let s: string = y
3341      y = nil
3342      let s2: string = y
3343    }
3344  }
3345}"#,
3346        );
3347        // s2 should fail because y was reassigned, invalidating the narrowing
3348        assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
3349        assert!(
3350            errs[0].contains("Type mismatch"),
3351            "expected type mismatch, got: {}",
3352            errs[0]
3353        );
3354    }
3355
3356    #[test]
3357    fn test_let_immutable_warning() {
3358        let all = check_source(
3359            r#"pipeline t(task) {
3360  let x = 42
3361  x = 43
3362}"#,
3363        );
3364        let warnings: Vec<_> = all
3365            .iter()
3366            .filter(|d| d.severity == DiagnosticSeverity::Warning)
3367            .collect();
3368        assert!(
3369            warnings.iter().any(|w| w.message.contains("immutable")),
3370            "expected immutability warning, got: {:?}",
3371            warnings
3372        );
3373    }
3374
3375    #[test]
3376    fn test_nested_narrowing() {
3377        let errs = errors(
3378            r#"pipeline t(task) {
3379  fn check(x: string | int | nil) {
3380    if x != nil {
3381      if type_of(x) == "int" {
3382        let i: int = x
3383      }
3384    }
3385  }
3386}"#,
3387        );
3388        assert!(errs.is_empty(), "got: {:?}", errs);
3389    }
3390
3391    #[test]
3392    fn test_match_narrows_arms() {
3393        let errs = errors(
3394            r#"pipeline t(task) {
3395  fn check(x: string | int) {
3396    match x {
3397      "hello" -> {
3398        let s: string = x
3399      }
3400      42 -> {
3401        let i: int = x
3402      }
3403      _ -> {}
3404    }
3405  }
3406}"#,
3407        );
3408        assert!(errs.is_empty(), "got: {:?}", errs);
3409    }
3410
3411    #[test]
3412    fn test_has_narrows_optional_field() {
3413        let errs = errors(
3414            r#"pipeline t(task) {
3415  fn check(x: {name?: string, age: int}) {
3416    if x.has("name") {
3417      let n: {name: string, age: int} = x
3418    }
3419  }
3420}"#,
3421        );
3422        assert!(errs.is_empty(), "got: {:?}", errs);
3423    }
3424}