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