Skip to main content

harn_parser/
typechecker.rs

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