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    fn bind_type_param(
1484        param_name: &str,
1485        concrete: &TypeExpr,
1486        bindings: &mut BTreeMap<String, TypeExpr>,
1487    ) -> Result<(), String> {
1488        if let Some(existing) = bindings.get(param_name) {
1489            if existing != concrete {
1490                return Err(format!(
1491                    "type parameter '{}' was inferred as both {} and {}",
1492                    param_name,
1493                    format_type(existing),
1494                    format_type(concrete)
1495                ));
1496            }
1497            return Ok(());
1498        }
1499        bindings.insert(param_name.to_string(), concrete.clone());
1500        Ok(())
1501    }
1502
1503    /// Recursively extract type parameter bindings from matching param/arg types.
1504    /// E.g., param_type=list<T> + arg_type=list<Dog> → binds T=Dog.
1505    fn extract_type_bindings(
1506        param_type: &TypeExpr,
1507        arg_type: &TypeExpr,
1508        type_params: &std::collections::BTreeSet<String>,
1509        bindings: &mut BTreeMap<String, TypeExpr>,
1510    ) -> Result<(), String> {
1511        match (param_type, arg_type) {
1512            (TypeExpr::Named(param_name), concrete) if type_params.contains(param_name) => {
1513                Self::bind_type_param(param_name, concrete, bindings)
1514            }
1515            (TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
1516                Self::extract_type_bindings(p_inner, a_inner, type_params, bindings)
1517            }
1518            (TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
1519                Self::extract_type_bindings(pk, ak, type_params, bindings)?;
1520                Self::extract_type_bindings(pv, av, type_params, bindings)
1521            }
1522            (TypeExpr::Shape(param_fields), TypeExpr::Shape(arg_fields)) => {
1523                for param_field in param_fields {
1524                    if let Some(arg_field) = arg_fields
1525                        .iter()
1526                        .find(|field| field.name == param_field.name)
1527                    {
1528                        Self::extract_type_bindings(
1529                            &param_field.type_expr,
1530                            &arg_field.type_expr,
1531                            type_params,
1532                            bindings,
1533                        )?;
1534                    }
1535                }
1536                Ok(())
1537            }
1538            (
1539                TypeExpr::FnType {
1540                    params: p_params,
1541                    return_type: p_ret,
1542                },
1543                TypeExpr::FnType {
1544                    params: a_params,
1545                    return_type: a_ret,
1546                },
1547            ) => {
1548                for (param, arg) in p_params.iter().zip(a_params.iter()) {
1549                    Self::extract_type_bindings(param, arg, type_params, bindings)?;
1550                }
1551                Self::extract_type_bindings(p_ret, a_ret, type_params, bindings)
1552            }
1553            _ => Ok(()),
1554        }
1555    }
1556
1557    fn apply_type_bindings(ty: &TypeExpr, bindings: &BTreeMap<String, TypeExpr>) -> TypeExpr {
1558        match ty {
1559            TypeExpr::Named(name) => bindings
1560                .get(name)
1561                .cloned()
1562                .unwrap_or_else(|| TypeExpr::Named(name.clone())),
1563            TypeExpr::Union(items) => TypeExpr::Union(
1564                items
1565                    .iter()
1566                    .map(|item| Self::apply_type_bindings(item, bindings))
1567                    .collect(),
1568            ),
1569            TypeExpr::Shape(fields) => TypeExpr::Shape(
1570                fields
1571                    .iter()
1572                    .map(|field| ShapeField {
1573                        name: field.name.clone(),
1574                        type_expr: Self::apply_type_bindings(&field.type_expr, bindings),
1575                        optional: field.optional,
1576                    })
1577                    .collect(),
1578            ),
1579            TypeExpr::List(inner) => {
1580                TypeExpr::List(Box::new(Self::apply_type_bindings(inner, bindings)))
1581            }
1582            TypeExpr::DictType(key, value) => TypeExpr::DictType(
1583                Box::new(Self::apply_type_bindings(key, bindings)),
1584                Box::new(Self::apply_type_bindings(value, bindings)),
1585            ),
1586            TypeExpr::FnType {
1587                params,
1588                return_type,
1589            } => TypeExpr::FnType {
1590                params: params
1591                    .iter()
1592                    .map(|param| Self::apply_type_bindings(param, bindings))
1593                    .collect(),
1594                return_type: Box::new(Self::apply_type_bindings(return_type, bindings)),
1595            },
1596        }
1597    }
1598
1599    fn infer_list_literal_type(&self, items: &[SNode], scope: &TypeScope) -> TypeExpr {
1600        let mut inferred: Option<TypeExpr> = None;
1601        for item in items {
1602            let Some(item_type) = self.infer_type(item, scope) else {
1603                return TypeExpr::Named("list".into());
1604            };
1605            inferred = Some(match inferred {
1606                None => item_type,
1607                Some(current) if current == item_type => current,
1608                Some(TypeExpr::Union(mut members)) => {
1609                    if !members.contains(&item_type) {
1610                        members.push(item_type);
1611                    }
1612                    TypeExpr::Union(members)
1613                }
1614                Some(current) => TypeExpr::Union(vec![current, item_type]),
1615            });
1616        }
1617        inferred
1618            .map(|item_type| TypeExpr::List(Box::new(item_type)))
1619            .unwrap_or_else(|| TypeExpr::Named("list".into()))
1620    }
1621
1622    /// Extract bidirectional type refinements from a condition expression.
1623    fn extract_refinements(condition: &SNode, scope: &TypeScope) -> Refinements {
1624        match &condition.node {
1625            // --- Nil checks and type_of checks ---
1626            Node::BinaryOp { op, left, right } if op == "!=" || op == "==" => {
1627                let nil_ref = Self::extract_nil_refinements(op, left, right, scope);
1628                if !nil_ref.truthy.is_empty() || !nil_ref.falsy.is_empty() {
1629                    return nil_ref;
1630                }
1631                let typeof_ref = Self::extract_typeof_refinements(op, left, right, scope);
1632                if !typeof_ref.truthy.is_empty() || !typeof_ref.falsy.is_empty() {
1633                    return typeof_ref;
1634                }
1635                Refinements::empty()
1636            }
1637
1638            // --- Logical AND: both must be true on truthy path ---
1639            Node::BinaryOp { op, left, right } if op == "&&" => {
1640                let left_ref = Self::extract_refinements(left, scope);
1641                let right_ref = Self::extract_refinements(right, scope);
1642                let mut truthy = left_ref.truthy;
1643                truthy.extend(right_ref.truthy);
1644                Refinements {
1645                    truthy,
1646                    falsy: vec![],
1647                }
1648            }
1649
1650            // --- Logical OR: both must be false on falsy path ---
1651            Node::BinaryOp { op, left, right } if op == "||" => {
1652                let left_ref = Self::extract_refinements(left, scope);
1653                let right_ref = Self::extract_refinements(right, scope);
1654                let mut falsy = left_ref.falsy;
1655                falsy.extend(right_ref.falsy);
1656                Refinements {
1657                    truthy: vec![],
1658                    falsy,
1659                }
1660            }
1661
1662            // --- Negation: swap truthy/falsy ---
1663            Node::UnaryOp { op, operand } if op == "!" => {
1664                Self::extract_refinements(operand, scope).inverted()
1665            }
1666
1667            // --- Truthiness: bare identifier in condition position ---
1668            Node::Identifier(name) => {
1669                if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1670                    if members
1671                        .iter()
1672                        .any(|m| matches!(m, TypeExpr::Named(n) if n == "nil"))
1673                    {
1674                        if let Some(narrowed) = remove_from_union(members, "nil") {
1675                            return Refinements {
1676                                truthy: vec![(name.clone(), Some(narrowed))],
1677                                falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1678                            };
1679                        }
1680                    }
1681                }
1682                Refinements::empty()
1683            }
1684
1685            // --- .has("key") on shapes ---
1686            Node::MethodCall {
1687                object,
1688                method,
1689                args,
1690            } if method == "has" && args.len() == 1 => {
1691                Self::extract_has_refinements(object, args, scope)
1692            }
1693
1694            _ => Refinements::empty(),
1695        }
1696    }
1697
1698    /// Extract nil-check refinements from `x != nil` / `x == nil` patterns.
1699    fn extract_nil_refinements(
1700        op: &str,
1701        left: &SNode,
1702        right: &SNode,
1703        scope: &TypeScope,
1704    ) -> Refinements {
1705        let var_node = if matches!(right.node, Node::NilLiteral) {
1706            left
1707        } else if matches!(left.node, Node::NilLiteral) {
1708            right
1709        } else {
1710            return Refinements::empty();
1711        };
1712
1713        if let Node::Identifier(name) = &var_node.node {
1714            if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1715                if let Some(narrowed) = remove_from_union(members, "nil") {
1716                    let neq_refs = Refinements {
1717                        truthy: vec![(name.clone(), Some(narrowed))],
1718                        falsy: vec![(name.clone(), Some(TypeExpr::Named("nil".into())))],
1719                    };
1720                    return if op == "!=" {
1721                        neq_refs
1722                    } else {
1723                        neq_refs.inverted()
1724                    };
1725                }
1726            }
1727        }
1728        Refinements::empty()
1729    }
1730
1731    /// Extract type_of refinements from `type_of(x) == "typename"` patterns.
1732    fn extract_typeof_refinements(
1733        op: &str,
1734        left: &SNode,
1735        right: &SNode,
1736        scope: &TypeScope,
1737    ) -> Refinements {
1738        let (var_name, type_name) = if let (Some(var), Node::StringLiteral(tn)) =
1739            (extract_type_of_var(left), &right.node)
1740        {
1741            (var, tn.clone())
1742        } else if let (Node::StringLiteral(tn), Some(var)) =
1743            (&left.node, extract_type_of_var(right))
1744        {
1745            (var, tn.clone())
1746        } else {
1747            return Refinements::empty();
1748        };
1749
1750        const KNOWN_TYPES: &[&str] = &[
1751            "int", "string", "float", "bool", "nil", "list", "dict", "closure",
1752        ];
1753        if !KNOWN_TYPES.contains(&type_name.as_str()) {
1754            return Refinements::empty();
1755        }
1756
1757        if let Some(Some(TypeExpr::Union(members))) = scope.get_var(&var_name) {
1758            let narrowed = narrow_to_single(members, &type_name);
1759            let remaining = remove_from_union(members, &type_name);
1760            if narrowed.is_some() || remaining.is_some() {
1761                let eq_refs = Refinements {
1762                    truthy: narrowed
1763                        .map(|n| vec![(var_name.clone(), Some(n))])
1764                        .unwrap_or_default(),
1765                    falsy: remaining
1766                        .map(|r| vec![(var_name.clone(), Some(r))])
1767                        .unwrap_or_default(),
1768                };
1769                return if op == "==" {
1770                    eq_refs
1771                } else {
1772                    eq_refs.inverted()
1773                };
1774            }
1775        }
1776        Refinements::empty()
1777    }
1778
1779    /// Extract .has("key") refinements on shape types.
1780    fn extract_has_refinements(object: &SNode, args: &[SNode], scope: &TypeScope) -> Refinements {
1781        if let Node::Identifier(var_name) = &object.node {
1782            if let Node::StringLiteral(key) = &args[0].node {
1783                if let Some(Some(TypeExpr::Shape(fields))) = scope.get_var(var_name) {
1784                    if fields.iter().any(|f| f.name == *key && f.optional) {
1785                        let narrowed_fields: Vec<ShapeField> = fields
1786                            .iter()
1787                            .map(|f| {
1788                                if f.name == *key {
1789                                    ShapeField {
1790                                        name: f.name.clone(),
1791                                        type_expr: f.type_expr.clone(),
1792                                        optional: false,
1793                                    }
1794                                } else {
1795                                    f.clone()
1796                                }
1797                            })
1798                            .collect();
1799                        return Refinements {
1800                            truthy: vec![(
1801                                var_name.clone(),
1802                                Some(TypeExpr::Shape(narrowed_fields)),
1803                            )],
1804                            falsy: vec![],
1805                        };
1806                    }
1807                }
1808            }
1809        }
1810        Refinements::empty()
1811    }
1812
1813    /// Check whether a block definitely exits (return/throw/break/continue).
1814    fn block_definitely_exits(stmts: &[SNode]) -> bool {
1815        stmts.iter().any(|s| match &s.node {
1816            Node::ReturnStmt { .. }
1817            | Node::ThrowStmt { .. }
1818            | Node::BreakStmt
1819            | Node::ContinueStmt => true,
1820            Node::IfElse {
1821                then_body,
1822                else_body: Some(else_body),
1823                ..
1824            } => Self::block_definitely_exits(then_body) && Self::block_definitely_exits(else_body),
1825            _ => false,
1826        })
1827    }
1828
1829    fn check_match_exhaustiveness(
1830        &mut self,
1831        value: &SNode,
1832        arms: &[MatchArm],
1833        scope: &TypeScope,
1834        span: Span,
1835    ) {
1836        // Detect pattern: match <expr>.variant { "VariantA" -> ... }
1837        let enum_name = match &value.node {
1838            Node::PropertyAccess { object, property } if property == "variant" => {
1839                // Infer the type of the object
1840                match self.infer_type(object, scope) {
1841                    Some(TypeExpr::Named(name)) => {
1842                        if scope.get_enum(&name).is_some() {
1843                            Some(name)
1844                        } else {
1845                            None
1846                        }
1847                    }
1848                    _ => None,
1849                }
1850            }
1851            _ => {
1852                // Direct match on an enum value: match <expr> { ... }
1853                match self.infer_type(value, scope) {
1854                    Some(TypeExpr::Named(name)) if scope.get_enum(&name).is_some() => Some(name),
1855                    _ => None,
1856                }
1857            }
1858        };
1859
1860        let Some(enum_name) = enum_name else {
1861            // Try union type exhaustiveness instead
1862            self.check_match_exhaustiveness_union(value, arms, scope, span);
1863            return;
1864        };
1865        let Some(variants) = scope.get_enum(&enum_name) else {
1866            return;
1867        };
1868
1869        // Collect variant names covered by match arms
1870        let mut covered: Vec<String> = Vec::new();
1871        let mut has_wildcard = false;
1872
1873        for arm in arms {
1874            match &arm.pattern.node {
1875                // String literal pattern (matching on .variant): "VariantA"
1876                Node::StringLiteral(s) => covered.push(s.clone()),
1877                // Identifier pattern acts as a wildcard/catch-all
1878                Node::Identifier(name) if name == "_" || !variants.contains(name) => {
1879                    has_wildcard = true;
1880                }
1881                // Direct enum construct pattern: EnumName.Variant
1882                Node::EnumConstruct { variant, .. } => covered.push(variant.clone()),
1883                // PropertyAccess pattern: EnumName.Variant (no args)
1884                Node::PropertyAccess { property, .. } => covered.push(property.clone()),
1885                _ => {
1886                    // Unknown pattern shape — conservatively treat as wildcard
1887                    has_wildcard = true;
1888                }
1889            }
1890        }
1891
1892        if has_wildcard {
1893            return;
1894        }
1895
1896        let missing: Vec<&String> = variants.iter().filter(|v| !covered.contains(v)).collect();
1897        if !missing.is_empty() {
1898            let missing_str = missing
1899                .iter()
1900                .map(|s| format!("\"{}\"", s))
1901                .collect::<Vec<_>>()
1902                .join(", ");
1903            self.warning_at(
1904                format!(
1905                    "Non-exhaustive match on enum {}: missing variants {}",
1906                    enum_name, missing_str
1907                ),
1908                span,
1909            );
1910        }
1911    }
1912
1913    /// Check exhaustiveness for match on union types (e.g. `string | int | nil`).
1914    fn check_match_exhaustiveness_union(
1915        &mut self,
1916        value: &SNode,
1917        arms: &[MatchArm],
1918        scope: &TypeScope,
1919        span: Span,
1920    ) {
1921        let Some(TypeExpr::Union(members)) = self.infer_type(value, scope) else {
1922            return;
1923        };
1924        // Only check unions of named types (string, int, nil, bool, etc.)
1925        if !members.iter().all(|m| matches!(m, TypeExpr::Named(_))) {
1926            return;
1927        }
1928
1929        let mut has_wildcard = false;
1930        let mut covered_types: Vec<String> = Vec::new();
1931
1932        for arm in arms {
1933            match &arm.pattern.node {
1934                // type_of(x) == "string" style patterns are common but hard to detect here
1935                // Literal patterns cover specific types
1936                Node::NilLiteral => covered_types.push("nil".into()),
1937                Node::BoolLiteral(_) => {
1938                    if !covered_types.contains(&"bool".into()) {
1939                        covered_types.push("bool".into());
1940                    }
1941                }
1942                Node::IntLiteral(_) => {
1943                    if !covered_types.contains(&"int".into()) {
1944                        covered_types.push("int".into());
1945                    }
1946                }
1947                Node::FloatLiteral(_) => {
1948                    if !covered_types.contains(&"float".into()) {
1949                        covered_types.push("float".into());
1950                    }
1951                }
1952                Node::StringLiteral(_) => {
1953                    if !covered_types.contains(&"string".into()) {
1954                        covered_types.push("string".into());
1955                    }
1956                }
1957                Node::Identifier(name) if name == "_" => {
1958                    has_wildcard = true;
1959                }
1960                _ => {
1961                    has_wildcard = true;
1962                }
1963            }
1964        }
1965
1966        if has_wildcard {
1967            return;
1968        }
1969
1970        let type_names: Vec<&str> = members
1971            .iter()
1972            .filter_map(|m| match m {
1973                TypeExpr::Named(n) => Some(n.as_str()),
1974                _ => None,
1975            })
1976            .collect();
1977        let missing: Vec<&&str> = type_names
1978            .iter()
1979            .filter(|t| !covered_types.iter().any(|c| c == **t))
1980            .collect();
1981        if !missing.is_empty() {
1982            let missing_str = missing
1983                .iter()
1984                .map(|s| s.to_string())
1985                .collect::<Vec<_>>()
1986                .join(", ");
1987            self.warning_at(
1988                format!(
1989                    "Non-exhaustive match on union type: missing {}",
1990                    missing_str
1991                ),
1992                span,
1993            );
1994        }
1995    }
1996
1997    fn check_call(&mut self, name: &str, args: &[SNode], scope: &mut TypeScope, span: Span) {
1998        // Check against known function signatures
1999        let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
2000        if let Some(sig) = scope.get_fn(name).cloned() {
2001            if !has_spread
2002                && !is_builtin(name)
2003                && !sig.has_rest
2004                && (args.len() < sig.required_params || args.len() > sig.params.len())
2005            {
2006                let expected = if sig.required_params == sig.params.len() {
2007                    format!("{}", sig.params.len())
2008                } else {
2009                    format!("{}-{}", sig.required_params, sig.params.len())
2010                };
2011                self.warning_at(
2012                    format!(
2013                        "Function '{}' expects {} arguments, got {}",
2014                        name,
2015                        expected,
2016                        args.len()
2017                    ),
2018                    span,
2019                );
2020            }
2021            // Build a scope that includes the function's generic type params
2022            // so they are treated as compatible with any concrete type.
2023            let call_scope = if sig.type_param_names.is_empty() {
2024                scope.clone()
2025            } else {
2026                let mut s = scope.child();
2027                for tp_name in &sig.type_param_names {
2028                    s.generic_type_params.insert(tp_name.clone());
2029                }
2030                s
2031            };
2032            let mut type_bindings: BTreeMap<String, TypeExpr> = BTreeMap::new();
2033            let type_param_set: std::collections::BTreeSet<String> =
2034                sig.type_param_names.iter().cloned().collect();
2035            for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
2036                if let Some(param_ty) = param_type {
2037                    if let Some(arg_ty) = self.infer_type(arg, scope) {
2038                        if let Err(message) = Self::extract_type_bindings(
2039                            param_ty,
2040                            &arg_ty,
2041                            &type_param_set,
2042                            &mut type_bindings,
2043                        ) {
2044                            self.error_at(message, arg.span);
2045                        }
2046                    }
2047                }
2048            }
2049            for (i, (arg, (param_name, param_type))) in
2050                args.iter().zip(sig.params.iter()).enumerate()
2051            {
2052                if let Some(expected) = param_type {
2053                    let actual = self.infer_type(arg, scope);
2054                    if let Some(actual) = &actual {
2055                        let expected = Self::apply_type_bindings(expected, &type_bindings);
2056                        if !self.types_compatible(&expected, actual, &call_scope) {
2057                            self.error_at(
2058                                format!(
2059                                    "Argument {} ('{}'): expected {}, got {}",
2060                                    i + 1,
2061                                    param_name,
2062                                    format_type(&expected),
2063                                    format_type(actual)
2064                                ),
2065                                arg.span,
2066                            );
2067                        }
2068                    }
2069                }
2070            }
2071            if !sig.where_clauses.is_empty() {
2072                for (type_param, bound) in &sig.where_clauses {
2073                    if let Some(concrete_type) = type_bindings.get(type_param) {
2074                        let concrete_name = format_type(concrete_type);
2075                        if let Some(reason) =
2076                            self.interface_mismatch_reason(&concrete_name, bound, scope)
2077                        {
2078                            self.error_at(
2079                                format!(
2080                                    "Type '{}' does not satisfy interface '{}': {} \
2081                                     (required by constraint `where {}: {}`)",
2082                                    concrete_name, bound, reason, type_param, bound
2083                                ),
2084                                span,
2085                            );
2086                        }
2087                    }
2088                }
2089            }
2090        }
2091        // Check args recursively
2092        for arg in args {
2093            self.check_node(arg, scope);
2094        }
2095    }
2096
2097    /// Infer the type of an expression.
2098    fn infer_type(&self, snode: &SNode, scope: &TypeScope) -> InferredType {
2099        match &snode.node {
2100            Node::IntLiteral(_) => Some(TypeExpr::Named("int".into())),
2101            Node::FloatLiteral(_) => Some(TypeExpr::Named("float".into())),
2102            Node::StringLiteral(_) | Node::InterpolatedString(_) => {
2103                Some(TypeExpr::Named("string".into()))
2104            }
2105            Node::BoolLiteral(_) => Some(TypeExpr::Named("bool".into())),
2106            Node::NilLiteral => Some(TypeExpr::Named("nil".into())),
2107            Node::ListLiteral(items) => Some(self.infer_list_literal_type(items, scope)),
2108            Node::DictLiteral(entries) => {
2109                // Infer shape type when all keys are string literals
2110                let mut fields = Vec::new();
2111                for entry in entries {
2112                    let key = match &entry.key.node {
2113                        Node::StringLiteral(key) | Node::Identifier(key) => key.clone(),
2114                        _ => return Some(TypeExpr::Named("dict".into())),
2115                    };
2116                    let val_type = self
2117                        .infer_type(&entry.value, scope)
2118                        .unwrap_or(TypeExpr::Named("nil".into()));
2119                    fields.push(ShapeField {
2120                        name: key,
2121                        type_expr: val_type,
2122                        optional: false,
2123                    });
2124                }
2125                if !fields.is_empty() {
2126                    Some(TypeExpr::Shape(fields))
2127                } else {
2128                    Some(TypeExpr::Named("dict".into()))
2129                }
2130            }
2131            Node::Closure { params, body, .. } => {
2132                // If all params are typed and we can infer a return type, produce FnType
2133                let all_typed = params.iter().all(|p| p.type_expr.is_some());
2134                if all_typed && !params.is_empty() {
2135                    let param_types: Vec<TypeExpr> =
2136                        params.iter().filter_map(|p| p.type_expr.clone()).collect();
2137                    // Try to infer return type from last expression in body
2138                    let ret = body.last().and_then(|last| self.infer_type(last, scope));
2139                    if let Some(ret_type) = ret {
2140                        return Some(TypeExpr::FnType {
2141                            params: param_types,
2142                            return_type: Box::new(ret_type),
2143                        });
2144                    }
2145                }
2146                Some(TypeExpr::Named("closure".into()))
2147            }
2148
2149            Node::Identifier(name) => scope.get_var(name).cloned().flatten(),
2150
2151            Node::FunctionCall { name, args } => {
2152                // Struct constructor calls return the struct type
2153                if scope.get_struct(name).is_some() {
2154                    return Some(TypeExpr::Named(name.clone()));
2155                }
2156                // Check user-defined function return types
2157                if let Some(sig) = scope.get_fn(name) {
2158                    let mut return_type = sig.return_type.clone();
2159                    if let Some(ty) = return_type.take() {
2160                        if sig.type_param_names.is_empty() {
2161                            return Some(ty);
2162                        }
2163                        let mut bindings = BTreeMap::new();
2164                        let type_param_set: std::collections::BTreeSet<String> =
2165                            sig.type_param_names.iter().cloned().collect();
2166                        for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
2167                            if let Some(param_ty) = param_type {
2168                                if let Some(arg_ty) = self.infer_type(arg, scope) {
2169                                    let _ = Self::extract_type_bindings(
2170                                        param_ty,
2171                                        &arg_ty,
2172                                        &type_param_set,
2173                                        &mut bindings,
2174                                    );
2175                                }
2176                            }
2177                        }
2178                        return Some(Self::apply_type_bindings(&ty, &bindings));
2179                    }
2180                    return None;
2181                }
2182                // Check builtin return types
2183                builtin_return_type(name)
2184            }
2185
2186            Node::BinaryOp { op, left, right } => {
2187                let lt = self.infer_type(left, scope);
2188                let rt = self.infer_type(right, scope);
2189                infer_binary_op_type(op, &lt, &rt)
2190            }
2191
2192            Node::UnaryOp { op, operand } => {
2193                let t = self.infer_type(operand, scope);
2194                match op.as_str() {
2195                    "!" => Some(TypeExpr::Named("bool".into())),
2196                    "-" => t, // negation preserves type
2197                    _ => None,
2198                }
2199            }
2200
2201            Node::Ternary {
2202                condition,
2203                true_expr,
2204                false_expr,
2205            } => {
2206                let refs = Self::extract_refinements(condition, scope);
2207
2208                let mut true_scope = scope.child();
2209                apply_refinements(&mut true_scope, &refs.truthy);
2210                let tt = self.infer_type(true_expr, &true_scope);
2211
2212                let mut false_scope = scope.child();
2213                apply_refinements(&mut false_scope, &refs.falsy);
2214                let ft = self.infer_type(false_expr, &false_scope);
2215
2216                match (&tt, &ft) {
2217                    (Some(a), Some(b)) if a == b => tt,
2218                    (Some(a), Some(b)) => Some(TypeExpr::Union(vec![a.clone(), b.clone()])),
2219                    (Some(_), None) => tt,
2220                    (None, Some(_)) => ft,
2221                    (None, None) => None,
2222                }
2223            }
2224
2225            Node::EnumConstruct { enum_name, .. } => Some(TypeExpr::Named(enum_name.clone())),
2226
2227            Node::PropertyAccess { object, property } => {
2228                // EnumName.Variant → infer as the enum type
2229                if let Node::Identifier(name) = &object.node {
2230                    if scope.get_enum(name).is_some() {
2231                        return Some(TypeExpr::Named(name.clone()));
2232                    }
2233                }
2234                // .variant on an enum value → string
2235                if property == "variant" {
2236                    let obj_type = self.infer_type(object, scope);
2237                    if let Some(TypeExpr::Named(name)) = &obj_type {
2238                        if scope.get_enum(name).is_some() {
2239                            return Some(TypeExpr::Named("string".into()));
2240                        }
2241                    }
2242                }
2243                // Shape field access: obj.field → field type
2244                let obj_type = self.infer_type(object, scope);
2245                if let Some(TypeExpr::Shape(fields)) = &obj_type {
2246                    if let Some(field) = fields.iter().find(|f| f.name == *property) {
2247                        return Some(field.type_expr.clone());
2248                    }
2249                }
2250                None
2251            }
2252
2253            Node::SubscriptAccess { object, index } => {
2254                let obj_type = self.infer_type(object, scope);
2255                match &obj_type {
2256                    Some(TypeExpr::List(inner)) => Some(*inner.clone()),
2257                    Some(TypeExpr::DictType(_, v)) => Some(*v.clone()),
2258                    Some(TypeExpr::Shape(fields)) => {
2259                        // If index is a string literal, look up the field type
2260                        if let Node::StringLiteral(key) = &index.node {
2261                            fields
2262                                .iter()
2263                                .find(|f| &f.name == key)
2264                                .map(|f| f.type_expr.clone())
2265                        } else {
2266                            None
2267                        }
2268                    }
2269                    Some(TypeExpr::Named(n)) if n == "list" => None,
2270                    Some(TypeExpr::Named(n)) if n == "dict" => None,
2271                    Some(TypeExpr::Named(n)) if n == "string" => {
2272                        Some(TypeExpr::Named("string".into()))
2273                    }
2274                    _ => None,
2275                }
2276            }
2277            Node::SliceAccess { object, .. } => {
2278                // Slicing a list returns the same list type; slicing a string returns string
2279                let obj_type = self.infer_type(object, scope);
2280                match &obj_type {
2281                    Some(TypeExpr::List(_)) => obj_type,
2282                    Some(TypeExpr::Named(n)) if n == "list" => obj_type,
2283                    Some(TypeExpr::Named(n)) if n == "string" => {
2284                        Some(TypeExpr::Named("string".into()))
2285                    }
2286                    _ => None,
2287                }
2288            }
2289            Node::MethodCall { object, method, .. }
2290            | Node::OptionalMethodCall { object, method, .. } => {
2291                let obj_type = self.infer_type(object, scope);
2292                let is_dict = matches!(&obj_type, Some(TypeExpr::Named(n)) if n == "dict")
2293                    || matches!(&obj_type, Some(TypeExpr::DictType(..)))
2294                    || matches!(&obj_type, Some(TypeExpr::Shape(_)));
2295                match method.as_str() {
2296                    // Shared: bool-returning methods
2297                    "contains" | "starts_with" | "ends_with" | "empty" | "has" | "any" | "all" => {
2298                        Some(TypeExpr::Named("bool".into()))
2299                    }
2300                    // Shared: int-returning methods
2301                    "count" | "index_of" => Some(TypeExpr::Named("int".into())),
2302                    // String methods
2303                    "trim" | "lowercase" | "uppercase" | "reverse" | "replace" | "substring"
2304                    | "pad_left" | "pad_right" | "repeat" | "join" => {
2305                        Some(TypeExpr::Named("string".into()))
2306                    }
2307                    "split" | "chars" => Some(TypeExpr::Named("list".into())),
2308                    // filter returns dict for dicts, list for lists
2309                    "filter" => {
2310                        if is_dict {
2311                            Some(TypeExpr::Named("dict".into()))
2312                        } else {
2313                            Some(TypeExpr::Named("list".into()))
2314                        }
2315                    }
2316                    // List methods
2317                    "map" | "flat_map" | "sort" => Some(TypeExpr::Named("list".into())),
2318                    "reduce" | "find" | "first" | "last" => None,
2319                    // Dict methods
2320                    "keys" | "values" | "entries" => Some(TypeExpr::Named("list".into())),
2321                    "merge" | "map_values" | "rekey" | "map_keys" => {
2322                        // Rekey/map_keys transform keys; resulting dict still keys-by-string.
2323                        // Preserve the value-type parameter when known so downstream code can
2324                        // still rely on dict<string, V> typing after a key-rename.
2325                        if let Some(TypeExpr::DictType(_, v)) = &obj_type {
2326                            Some(TypeExpr::DictType(
2327                                Box::new(TypeExpr::Named("string".into())),
2328                                v.clone(),
2329                            ))
2330                        } else {
2331                            Some(TypeExpr::Named("dict".into()))
2332                        }
2333                    }
2334                    // Conversions
2335                    "to_string" => Some(TypeExpr::Named("string".into())),
2336                    "to_int" => Some(TypeExpr::Named("int".into())),
2337                    "to_float" => Some(TypeExpr::Named("float".into())),
2338                    _ => None,
2339                }
2340            }
2341
2342            // TryOperator on Result<T, E> produces T
2343            Node::TryOperator { operand } => {
2344                match self.infer_type(operand, scope) {
2345                    Some(TypeExpr::Named(name)) if name == "Result" => None, // unknown inner type
2346                    _ => None,
2347                }
2348            }
2349
2350            _ => None,
2351        }
2352    }
2353
2354    /// Check if two types are compatible (actual can be assigned to expected).
2355    fn types_compatible(&self, expected: &TypeExpr, actual: &TypeExpr, scope: &TypeScope) -> bool {
2356        // Generic type parameters match anything.
2357        if let TypeExpr::Named(name) = expected {
2358            if scope.is_generic_type_param(name) {
2359                return true;
2360            }
2361        }
2362        if let TypeExpr::Named(name) = actual {
2363            if scope.is_generic_type_param(name) {
2364                return true;
2365            }
2366        }
2367        let expected = self.resolve_alias(expected, scope);
2368        let actual = self.resolve_alias(actual, scope);
2369
2370        // Interface satisfaction: if expected is an interface name, check if actual type
2371        // has all required methods (Go-style implicit satisfaction).
2372        if let TypeExpr::Named(iface_name) = &expected {
2373            if scope.get_interface(iface_name).is_some() {
2374                if let TypeExpr::Named(type_name) = &actual {
2375                    return self.satisfies_interface(type_name, iface_name, scope);
2376                }
2377                return false;
2378            }
2379        }
2380
2381        match (&expected, &actual) {
2382            (TypeExpr::Named(a), TypeExpr::Named(b)) => a == b || (a == "float" && b == "int"),
2383            // Union-to-Union: every member of actual must be compatible with
2384            // at least one member of expected.
2385            (TypeExpr::Union(exp_members), TypeExpr::Union(act_members)) => {
2386                act_members.iter().all(|am| {
2387                    exp_members
2388                        .iter()
2389                        .any(|em| self.types_compatible(em, am, scope))
2390                })
2391            }
2392            (TypeExpr::Union(members), actual_type) => members
2393                .iter()
2394                .any(|m| self.types_compatible(m, actual_type, scope)),
2395            (expected_type, TypeExpr::Union(members)) => members
2396                .iter()
2397                .all(|m| self.types_compatible(expected_type, m, scope)),
2398            (TypeExpr::Shape(_), TypeExpr::Named(n)) if n == "dict" => true,
2399            (TypeExpr::Named(n), TypeExpr::Shape(_)) if n == "dict" => true,
2400            (TypeExpr::Shape(ef), TypeExpr::Shape(af)) => ef.iter().all(|expected_field| {
2401                if expected_field.optional {
2402                    return true;
2403                }
2404                af.iter().any(|actual_field| {
2405                    actual_field.name == expected_field.name
2406                        && self.types_compatible(
2407                            &expected_field.type_expr,
2408                            &actual_field.type_expr,
2409                            scope,
2410                        )
2411                })
2412            }),
2413            // dict<K, V> expected, Shape actual → all field values must match V
2414            (TypeExpr::DictType(ek, ev), TypeExpr::Shape(af)) => {
2415                let keys_ok = matches!(ek.as_ref(), TypeExpr::Named(n) if n == "string");
2416                keys_ok
2417                    && af
2418                        .iter()
2419                        .all(|f| self.types_compatible(ev, &f.type_expr, scope))
2420            }
2421            // Shape expected, dict<K, V> actual → gradual: allow since dict may have the fields
2422            (TypeExpr::Shape(_), TypeExpr::DictType(_, _)) => true,
2423            (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
2424                self.types_compatible(expected_inner, actual_inner, scope)
2425            }
2426            (TypeExpr::Named(n), TypeExpr::List(_)) if n == "list" => true,
2427            (TypeExpr::List(_), TypeExpr::Named(n)) if n == "list" => true,
2428            (TypeExpr::DictType(ek, ev), TypeExpr::DictType(ak, av)) => {
2429                self.types_compatible(ek, ak, scope) && self.types_compatible(ev, av, scope)
2430            }
2431            (TypeExpr::Named(n), TypeExpr::DictType(_, _)) if n == "dict" => true,
2432            (TypeExpr::DictType(_, _), TypeExpr::Named(n)) if n == "dict" => true,
2433            // FnType compatibility: params match positionally and return types match
2434            (
2435                TypeExpr::FnType {
2436                    params: ep,
2437                    return_type: er,
2438                },
2439                TypeExpr::FnType {
2440                    params: ap,
2441                    return_type: ar,
2442                },
2443            ) => {
2444                ep.len() == ap.len()
2445                    && ep
2446                        .iter()
2447                        .zip(ap.iter())
2448                        .all(|(e, a)| self.types_compatible(e, a, scope))
2449                    && self.types_compatible(er, ar, scope)
2450            }
2451            // FnType is compatible with Named("closure") for backward compat
2452            (TypeExpr::FnType { .. }, TypeExpr::Named(n)) if n == "closure" => true,
2453            (TypeExpr::Named(n), TypeExpr::FnType { .. }) if n == "closure" => true,
2454            _ => false,
2455        }
2456    }
2457
2458    fn resolve_alias<'a>(&self, ty: &'a TypeExpr, scope: &'a TypeScope) -> TypeExpr {
2459        if let TypeExpr::Named(name) = ty {
2460            if let Some(resolved) = scope.resolve_type(name) {
2461                return resolved.clone();
2462            }
2463        }
2464        ty.clone()
2465    }
2466
2467    fn error_at(&mut self, message: String, span: Span) {
2468        self.diagnostics.push(TypeDiagnostic {
2469            message,
2470            severity: DiagnosticSeverity::Error,
2471            span: Some(span),
2472            help: None,
2473            fix: None,
2474        });
2475    }
2476
2477    #[allow(dead_code)]
2478    fn error_at_with_help(&mut self, message: String, span: Span, help: String) {
2479        self.diagnostics.push(TypeDiagnostic {
2480            message,
2481            severity: DiagnosticSeverity::Error,
2482            span: Some(span),
2483            help: Some(help),
2484            fix: None,
2485        });
2486    }
2487
2488    fn error_at_with_fix(&mut self, message: String, span: Span, fix: Vec<FixEdit>) {
2489        self.diagnostics.push(TypeDiagnostic {
2490            message,
2491            severity: DiagnosticSeverity::Error,
2492            span: Some(span),
2493            help: None,
2494            fix: Some(fix),
2495        });
2496    }
2497
2498    fn warning_at(&mut self, message: String, span: Span) {
2499        self.diagnostics.push(TypeDiagnostic {
2500            message,
2501            severity: DiagnosticSeverity::Warning,
2502            span: Some(span),
2503            help: None,
2504            fix: None,
2505        });
2506    }
2507
2508    #[allow(dead_code)]
2509    fn warning_at_with_help(&mut self, message: String, span: Span, help: String) {
2510        self.diagnostics.push(TypeDiagnostic {
2511            message,
2512            severity: DiagnosticSeverity::Warning,
2513            span: Some(span),
2514            help: Some(help),
2515            fix: None,
2516        });
2517    }
2518
2519    /// Recursively validate binary operations in an expression tree.
2520    /// Unlike `check_node`, this only checks BinaryOp type compatibility
2521    /// without triggering other validations (e.g., function call arg checks).
2522    fn check_binops(&mut self, snode: &SNode, scope: &mut TypeScope) {
2523        match &snode.node {
2524            Node::BinaryOp { op, left, right } => {
2525                self.check_binops(left, scope);
2526                self.check_binops(right, scope);
2527                let lt = self.infer_type(left, scope);
2528                let rt = self.infer_type(right, scope);
2529                if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (&lt, &rt) {
2530                    let span = snode.span;
2531                    match op.as_str() {
2532                        "+" => {
2533                            let valid = matches!(
2534                                (l.as_str(), r.as_str()),
2535                                ("int" | "float", "int" | "float")
2536                                    | ("string", "string")
2537                                    | ("list", "list")
2538                                    | ("dict", "dict")
2539                            );
2540                            if !valid {
2541                                let msg =
2542                                    format!("Operator '+' is not valid for types {} and {}", l, r);
2543                                let fix = if l == "string" || r == "string" {
2544                                    self.build_interpolation_fix(left, right, l == "string", span)
2545                                } else {
2546                                    None
2547                                };
2548                                if let Some(fix) = fix {
2549                                    self.error_at_with_fix(msg, span, fix);
2550                                } else {
2551                                    self.error_at(msg, span);
2552                                }
2553                            }
2554                        }
2555                        "-" | "/" | "%" => {
2556                            let numeric = ["int", "float"];
2557                            if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
2558                                self.error_at(
2559                                    format!(
2560                                        "Operator '{}' requires numeric operands, got {} and {}",
2561                                        op, l, r
2562                                    ),
2563                                    span,
2564                                );
2565                            }
2566                        }
2567                        "*" => {
2568                            let numeric = ["int", "float"];
2569                            let is_numeric =
2570                                numeric.contains(&l.as_str()) && numeric.contains(&r.as_str());
2571                            let is_string_repeat =
2572                                (l == "string" && r == "int") || (l == "int" && r == "string");
2573                            if !is_numeric && !is_string_repeat {
2574                                self.error_at(
2575                                    format!(
2576                                        "Operator '*' requires numeric operands or string * int, got {} and {}",
2577                                        l, r
2578                                    ),
2579                                    span,
2580                                );
2581                            }
2582                        }
2583                        _ => {}
2584                    }
2585                }
2586            }
2587            // Recurse into sub-expressions that might contain BinaryOps
2588            Node::UnaryOp { operand, .. } => self.check_binops(operand, scope),
2589            _ => {}
2590        }
2591    }
2592
2593    /// Build a fix that converts `"str" + expr` or `expr + "str"` to string interpolation.
2594    fn build_interpolation_fix(
2595        &self,
2596        left: &SNode,
2597        right: &SNode,
2598        left_is_string: bool,
2599        expr_span: Span,
2600    ) -> Option<Vec<FixEdit>> {
2601        let src = self.source.as_ref()?;
2602        let (str_node, other_node) = if left_is_string {
2603            (left, right)
2604        } else {
2605            (right, left)
2606        };
2607        let str_text = src.get(str_node.span.start..str_node.span.end)?;
2608        let other_text = src.get(other_node.span.start..other_node.span.end)?;
2609        // Only handle simple double-quoted strings (not multiline/raw)
2610        let inner = str_text.strip_prefix('"')?.strip_suffix('"')?;
2611        // Skip if the expression contains characters that would break interpolation
2612        if other_text.contains('}') || other_text.contains('"') {
2613            return None;
2614        }
2615        let replacement = if left_is_string {
2616            format!("\"{inner}${{{other_text}}}\"")
2617        } else {
2618            format!("\"${{{other_text}}}{inner}\"")
2619        };
2620        Some(vec![FixEdit {
2621            span: expr_span,
2622            replacement,
2623        }])
2624    }
2625}
2626
2627impl Default for TypeChecker {
2628    fn default() -> Self {
2629        Self::new()
2630    }
2631}
2632
2633/// Infer the result type of a binary operation.
2634fn infer_binary_op_type(op: &str, left: &InferredType, right: &InferredType) -> InferredType {
2635    match op {
2636        "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
2637            Some(TypeExpr::Named("bool".into()))
2638        }
2639        "+" => match (left, right) {
2640            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2641                match (l.as_str(), r.as_str()) {
2642                    ("int", "int") => Some(TypeExpr::Named("int".into())),
2643                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2644                    ("string", "string") => Some(TypeExpr::Named("string".into())),
2645                    ("list", "list") => Some(TypeExpr::Named("list".into())),
2646                    ("dict", "dict") => Some(TypeExpr::Named("dict".into())),
2647                    _ => None,
2648                }
2649            }
2650            _ => None,
2651        },
2652        "-" | "/" | "%" => match (left, right) {
2653            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2654                match (l.as_str(), r.as_str()) {
2655                    ("int", "int") => Some(TypeExpr::Named("int".into())),
2656                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2657                    _ => None,
2658                }
2659            }
2660            _ => None,
2661        },
2662        "*" => match (left, right) {
2663            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
2664                match (l.as_str(), r.as_str()) {
2665                    ("string", "int") | ("int", "string") => Some(TypeExpr::Named("string".into())),
2666                    ("int", "int") => Some(TypeExpr::Named("int".into())),
2667                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
2668                    _ => None,
2669                }
2670            }
2671            _ => None,
2672        },
2673        "??" => match (left, right) {
2674            // Union containing nil: strip nil, use non-nil members
2675            (Some(TypeExpr::Union(members)), _) => {
2676                let non_nil: Vec<_> = members
2677                    .iter()
2678                    .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
2679                    .cloned()
2680                    .collect();
2681                if non_nil.len() == 1 {
2682                    Some(non_nil[0].clone())
2683                } else if non_nil.is_empty() {
2684                    right.clone()
2685                } else {
2686                    Some(TypeExpr::Union(non_nil))
2687                }
2688            }
2689            // Left is nil: result is always the right side
2690            (Some(TypeExpr::Named(n)), _) if n == "nil" => right.clone(),
2691            // Left is a known non-nil type: right is unreachable, preserve left
2692            (Some(l), _) => Some(l.clone()),
2693            // Unknown left: use right as best guess
2694            (None, _) => right.clone(),
2695        },
2696        "|>" => None,
2697        _ => None,
2698    }
2699}
2700
2701/// Format a type expression for display in error messages.
2702/// Produce a detail string describing why a Shape type is incompatible with
2703/// another Shape type — e.g. "missing field 'age' (int)" or "field 'name'
2704/// has type int, expected string".  Returns `None` if both types are not shapes.
2705pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
2706    if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
2707        let mut details = Vec::new();
2708        for field in ef {
2709            if field.optional {
2710                continue;
2711            }
2712            match af.iter().find(|f| f.name == field.name) {
2713                None => details.push(format!(
2714                    "missing field '{}' ({})",
2715                    field.name,
2716                    format_type(&field.type_expr)
2717                )),
2718                Some(actual_field) => {
2719                    let e_str = format_type(&field.type_expr);
2720                    let a_str = format_type(&actual_field.type_expr);
2721                    if e_str != a_str {
2722                        details.push(format!(
2723                            "field '{}' has type {}, expected {}",
2724                            field.name, a_str, e_str
2725                        ));
2726                    }
2727                }
2728            }
2729        }
2730        if details.is_empty() {
2731            None
2732        } else {
2733            Some(details.join("; "))
2734        }
2735    } else {
2736        None
2737    }
2738}
2739
2740/// Returns true when the type is obvious from the RHS expression
2741/// (e.g. `let x = 42` is obviously int — no hint needed).
2742fn is_obvious_type(value: &SNode, _ty: &TypeExpr) -> bool {
2743    matches!(
2744        &value.node,
2745        Node::IntLiteral(_)
2746            | Node::FloatLiteral(_)
2747            | Node::StringLiteral(_)
2748            | Node::BoolLiteral(_)
2749            | Node::NilLiteral
2750            | Node::ListLiteral(_)
2751            | Node::DictLiteral(_)
2752            | Node::InterpolatedString(_)
2753    )
2754}
2755
2756pub fn format_type(ty: &TypeExpr) -> String {
2757    match ty {
2758        TypeExpr::Named(n) => n.clone(),
2759        TypeExpr::Union(types) => types
2760            .iter()
2761            .map(format_type)
2762            .collect::<Vec<_>>()
2763            .join(" | "),
2764        TypeExpr::Shape(fields) => {
2765            let inner: Vec<String> = fields
2766                .iter()
2767                .map(|f| {
2768                    let opt = if f.optional { "?" } else { "" };
2769                    format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
2770                })
2771                .collect();
2772            format!("{{{}}}", inner.join(", "))
2773        }
2774        TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
2775        TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
2776        TypeExpr::FnType {
2777            params,
2778            return_type,
2779        } => {
2780            let params_str = params
2781                .iter()
2782                .map(format_type)
2783                .collect::<Vec<_>>()
2784                .join(", ");
2785            format!("fn({}) -> {}", params_str, format_type(return_type))
2786        }
2787    }
2788}
2789
2790/// Remove a named type from a union, collapsing single-element unions.
2791fn remove_from_union(members: &[TypeExpr], to_remove: &str) -> InferredType {
2792    let remaining: Vec<TypeExpr> = members
2793        .iter()
2794        .filter(|m| !matches!(m, TypeExpr::Named(n) if n == to_remove))
2795        .cloned()
2796        .collect();
2797    match remaining.len() {
2798        0 => None,
2799        1 => Some(remaining.into_iter().next().unwrap()),
2800        _ => Some(TypeExpr::Union(remaining)),
2801    }
2802}
2803
2804/// Narrow a union to just one named type, if that type is a member.
2805fn narrow_to_single(members: &[TypeExpr], target: &str) -> InferredType {
2806    if members
2807        .iter()
2808        .any(|m| matches!(m, TypeExpr::Named(n) if n == target))
2809    {
2810        Some(TypeExpr::Named(target.to_string()))
2811    } else {
2812        None
2813    }
2814}
2815
2816/// Extract the variable name from a `type_of(x)` call.
2817fn extract_type_of_var(node: &SNode) -> Option<String> {
2818    if let Node::FunctionCall { name, args } = &node.node {
2819        if name == "type_of" && args.len() == 1 {
2820            if let Node::Identifier(var) = &args[0].node {
2821                return Some(var.clone());
2822            }
2823        }
2824    }
2825    None
2826}
2827
2828/// Apply a list of refinements to a scope, tracking pre-narrowing types.
2829fn apply_refinements(scope: &mut TypeScope, refinements: &[(String, InferredType)]) {
2830    for (var_name, narrowed_type) in refinements {
2831        // Save the pre-narrowing type so we can restore it on reassignment
2832        if !scope.narrowed_vars.contains_key(var_name) {
2833            if let Some(original) = scope.get_var(var_name).cloned() {
2834                scope.narrowed_vars.insert(var_name.clone(), original);
2835            }
2836        }
2837        scope.define_var(var_name, narrowed_type.clone());
2838    }
2839}
2840
2841#[cfg(test)]
2842mod tests {
2843    use super::*;
2844    use crate::Parser;
2845    use harn_lexer::Lexer;
2846
2847    fn check_source(source: &str) -> Vec<TypeDiagnostic> {
2848        let mut lexer = Lexer::new(source);
2849        let tokens = lexer.tokenize().unwrap();
2850        let mut parser = Parser::new(tokens);
2851        let program = parser.parse().unwrap();
2852        TypeChecker::new().check(&program)
2853    }
2854
2855    fn errors(source: &str) -> Vec<String> {
2856        check_source(source)
2857            .into_iter()
2858            .filter(|d| d.severity == DiagnosticSeverity::Error)
2859            .map(|d| d.message)
2860            .collect()
2861    }
2862
2863    #[test]
2864    fn test_no_errors_for_untyped_code() {
2865        let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
2866        assert!(errs.is_empty());
2867    }
2868
2869    #[test]
2870    fn test_correct_typed_let() {
2871        let errs = errors("pipeline t(task) { let x: int = 42 }");
2872        assert!(errs.is_empty());
2873    }
2874
2875    #[test]
2876    fn test_type_mismatch_let() {
2877        let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
2878        assert_eq!(errs.len(), 1);
2879        assert!(errs[0].contains("Type mismatch"));
2880        assert!(errs[0].contains("int"));
2881        assert!(errs[0].contains("string"));
2882    }
2883
2884    #[test]
2885    fn test_correct_typed_fn() {
2886        let errs = errors(
2887            "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
2888        );
2889        assert!(errs.is_empty());
2890    }
2891
2892    #[test]
2893    fn test_fn_arg_type_mismatch() {
2894        let errs = errors(
2895            r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
2896add("hello", 2) }"#,
2897        );
2898        assert_eq!(errs.len(), 1);
2899        assert!(errs[0].contains("Argument 1"));
2900        assert!(errs[0].contains("expected int"));
2901    }
2902
2903    #[test]
2904    fn test_return_type_mismatch() {
2905        let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
2906        assert_eq!(errs.len(), 1);
2907        assert!(errs[0].contains("Return type mismatch"));
2908    }
2909
2910    #[test]
2911    fn test_union_type_compatible() {
2912        let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
2913        assert!(errs.is_empty());
2914    }
2915
2916    #[test]
2917    fn test_union_type_mismatch() {
2918        let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
2919        assert_eq!(errs.len(), 1);
2920        assert!(errs[0].contains("Type mismatch"));
2921    }
2922
2923    #[test]
2924    fn test_type_inference_propagation() {
2925        let errs = errors(
2926            r#"pipeline t(task) {
2927  fn add(a: int, b: int) -> int { return a + b }
2928  let result: string = add(1, 2)
2929}"#,
2930        );
2931        assert_eq!(errs.len(), 1);
2932        assert!(errs[0].contains("Type mismatch"));
2933        assert!(errs[0].contains("string"));
2934        assert!(errs[0].contains("int"));
2935    }
2936
2937    #[test]
2938    fn test_generic_return_type_instantiates_from_callsite() {
2939        let errs = errors(
2940            r#"pipeline t(task) {
2941  fn identity<T>(x: T) -> T { return x }
2942  fn first<T>(items: list<T>) -> T { return items[0] }
2943  let n: int = identity(42)
2944  let s: string = first(["a", "b"])
2945}"#,
2946        );
2947        assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
2948    }
2949
2950    #[test]
2951    fn test_generic_type_param_must_bind_consistently() {
2952        let errs = errors(
2953            r#"pipeline t(task) {
2954  fn keep<T>(a: T, b: T) -> T { return a }
2955  keep(1, "x")
2956}"#,
2957        );
2958        assert_eq!(errs.len(), 2, "expected 2 errors, got: {:?}", errs);
2959        assert!(
2960            errs.iter()
2961                .any(|err| err.contains("type parameter 'T' was inferred as both int and string")),
2962            "missing generic binding conflict error: {:?}",
2963            errs
2964        );
2965        assert!(
2966            errs.iter()
2967                .any(|err| err.contains("Argument 2 ('b'): expected int, got string")),
2968            "missing instantiated argument mismatch error: {:?}",
2969            errs
2970        );
2971    }
2972
2973    #[test]
2974    fn test_generic_list_binding_propagates_element_type() {
2975        let errs = errors(
2976            r#"pipeline t(task) {
2977  fn first<T>(items: list<T>) -> T { return items[0] }
2978  let bad: string = first([1, 2, 3])
2979}"#,
2980        );
2981        assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
2982        assert!(errs[0].contains("declared as string, but assigned int"));
2983    }
2984
2985    #[test]
2986    fn test_builtin_return_type_inference() {
2987        let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
2988        assert_eq!(errs.len(), 1);
2989        assert!(errs[0].contains("string"));
2990        assert!(errs[0].contains("int"));
2991    }
2992
2993    #[test]
2994    fn test_workflow_and_transcript_builtins_are_known() {
2995        let errs = errors(
2996            r#"pipeline t(task) {
2997  let flow = workflow_graph({name: "demo", entry: "act", nodes: {act: {kind: "stage"}}})
2998  let report: dict = workflow_policy_report(flow, {tools: tool_registry(), capabilities: {workspace: ["read_text"]}})
2999  let run: dict = workflow_execute("task", flow, [], {})
3000  let tree: dict = load_run_tree("run.json")
3001  let fixture: dict = run_record_fixture(run?.run)
3002  let suite: dict = run_record_eval_suite([{run: run?.run, fixture: fixture}])
3003  let diff: dict = run_record_diff(run?.run, run?.run)
3004  let manifest: dict = eval_suite_manifest({cases: [{run_path: "run.json"}]})
3005  let suite_report: dict = eval_suite_run(manifest)
3006  let wf: dict = artifact_workspace_file("src/main.rs", "fn main() {}", {source: "host"})
3007  let snap: dict = artifact_workspace_snapshot(["src/main.rs"], "snapshot")
3008  let selection: dict = artifact_editor_selection("src/main.rs", "main")
3009  let verify: dict = artifact_verification_result("verify", "ok")
3010  let test_result: dict = artifact_test_result("tests", "pass")
3011  let cmd: dict = artifact_command_result("cargo test", {status: 0})
3012  let patch: dict = artifact_diff("src/main.rs", "old", "new")
3013  let git: dict = artifact_git_diff("diff --git a b")
3014  let review: dict = artifact_diff_review(patch, "review me")
3015  let decision: dict = artifact_review_decision(review, "accepted")
3016  let proposal: dict = artifact_patch_proposal(review, "*** Begin Patch")
3017  let bundle: dict = artifact_verification_bundle("checks", [{name: "fmt", ok: true}])
3018  let apply: dict = artifact_apply_intent(review, "apply")
3019  let transcript = transcript_reset({metadata: {source: "test"}})
3020  let visible: string = transcript_render_visible(transcript_archive(transcript))
3021  let events: list = transcript_events(transcript)
3022  let context: string = artifact_context([], {max_artifacts: 1})
3023  println(report)
3024  println(run)
3025  println(tree)
3026  println(fixture)
3027  println(suite)
3028  println(diff)
3029  println(manifest)
3030  println(suite_report)
3031  println(wf)
3032  println(snap)
3033  println(selection)
3034  println(verify)
3035  println(test_result)
3036  println(cmd)
3037  println(patch)
3038  println(git)
3039  println(review)
3040  println(decision)
3041  println(proposal)
3042  println(bundle)
3043  println(apply)
3044  println(visible)
3045  println(events)
3046  println(context)
3047}"#,
3048        );
3049        assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
3050    }
3051
3052    #[test]
3053    fn test_binary_op_type_inference() {
3054        let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
3055        assert_eq!(errs.len(), 1);
3056    }
3057
3058    #[test]
3059    fn test_comparison_returns_bool() {
3060        let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
3061        assert!(errs.is_empty());
3062    }
3063
3064    #[test]
3065    fn test_int_float_promotion() {
3066        let errs = errors("pipeline t(task) { let x: float = 42 }");
3067        assert!(errs.is_empty());
3068    }
3069
3070    #[test]
3071    fn test_untyped_code_no_errors() {
3072        let errs = errors(
3073            r#"pipeline t(task) {
3074  fn process(data) {
3075    let result = data + " processed"
3076    return result
3077  }
3078  log(process("hello"))
3079}"#,
3080        );
3081        assert!(errs.is_empty());
3082    }
3083
3084    #[test]
3085    fn test_type_alias() {
3086        let errs = errors(
3087            r#"pipeline t(task) {
3088  type Name = string
3089  let x: Name = "hello"
3090}"#,
3091        );
3092        assert!(errs.is_empty());
3093    }
3094
3095    #[test]
3096    fn test_type_alias_mismatch() {
3097        let errs = errors(
3098            r#"pipeline t(task) {
3099  type Name = string
3100  let x: Name = 42
3101}"#,
3102        );
3103        assert_eq!(errs.len(), 1);
3104    }
3105
3106    #[test]
3107    fn test_assignment_type_check() {
3108        let errs = errors(
3109            r#"pipeline t(task) {
3110  var x: int = 0
3111  x = "hello"
3112}"#,
3113        );
3114        assert_eq!(errs.len(), 1);
3115        assert!(errs[0].contains("cannot assign string"));
3116    }
3117
3118    #[test]
3119    fn test_covariance_int_to_float_in_fn() {
3120        let errs = errors(
3121            "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
3122        );
3123        assert!(errs.is_empty());
3124    }
3125
3126    #[test]
3127    fn test_covariance_return_type() {
3128        let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
3129        assert!(errs.is_empty());
3130    }
3131
3132    #[test]
3133    fn test_no_contravariance_float_to_int() {
3134        let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
3135        assert_eq!(errs.len(), 1);
3136    }
3137
3138    // --- Exhaustiveness checking tests ---
3139
3140    fn warnings(source: &str) -> Vec<String> {
3141        check_source(source)
3142            .into_iter()
3143            .filter(|d| d.severity == DiagnosticSeverity::Warning)
3144            .map(|d| d.message)
3145            .collect()
3146    }
3147
3148    #[test]
3149    fn test_exhaustive_match_no_warning() {
3150        let warns = warnings(
3151            r#"pipeline t(task) {
3152  enum Color { Red, Green, Blue }
3153  let c = Color.Red
3154  match c.variant {
3155    "Red" -> { log("r") }
3156    "Green" -> { log("g") }
3157    "Blue" -> { log("b") }
3158  }
3159}"#,
3160        );
3161        let exhaustive_warns: Vec<_> = warns
3162            .iter()
3163            .filter(|w| w.contains("Non-exhaustive"))
3164            .collect();
3165        assert!(exhaustive_warns.is_empty());
3166    }
3167
3168    #[test]
3169    fn test_non_exhaustive_match_warning() {
3170        let warns = warnings(
3171            r#"pipeline t(task) {
3172  enum Color { Red, Green, Blue }
3173  let c = Color.Red
3174  match c.variant {
3175    "Red" -> { log("r") }
3176    "Green" -> { log("g") }
3177  }
3178}"#,
3179        );
3180        let exhaustive_warns: Vec<_> = warns
3181            .iter()
3182            .filter(|w| w.contains("Non-exhaustive"))
3183            .collect();
3184        assert_eq!(exhaustive_warns.len(), 1);
3185        assert!(exhaustive_warns[0].contains("Blue"));
3186    }
3187
3188    #[test]
3189    fn test_non_exhaustive_multiple_missing() {
3190        let warns = warnings(
3191            r#"pipeline t(task) {
3192  enum Status { Active, Inactive, Pending }
3193  let s = Status.Active
3194  match s.variant {
3195    "Active" -> { log("a") }
3196  }
3197}"#,
3198        );
3199        let exhaustive_warns: Vec<_> = warns
3200            .iter()
3201            .filter(|w| w.contains("Non-exhaustive"))
3202            .collect();
3203        assert_eq!(exhaustive_warns.len(), 1);
3204        assert!(exhaustive_warns[0].contains("Inactive"));
3205        assert!(exhaustive_warns[0].contains("Pending"));
3206    }
3207
3208    #[test]
3209    fn test_enum_construct_type_inference() {
3210        let errs = errors(
3211            r#"pipeline t(task) {
3212  enum Color { Red, Green, Blue }
3213  let c: Color = Color.Red
3214}"#,
3215        );
3216        assert!(errs.is_empty());
3217    }
3218
3219    // --- Type narrowing tests ---
3220
3221    #[test]
3222    fn test_nil_coalescing_strips_nil() {
3223        // After ??, nil should be stripped from the type
3224        let errs = errors(
3225            r#"pipeline t(task) {
3226  let x: string | nil = nil
3227  let y: string = x ?? "default"
3228}"#,
3229        );
3230        assert!(errs.is_empty());
3231    }
3232
3233    #[test]
3234    fn test_shape_mismatch_detail_missing_field() {
3235        let errs = errors(
3236            r#"pipeline t(task) {
3237  let x: {name: string, age: int} = {name: "hello"}
3238}"#,
3239        );
3240        assert_eq!(errs.len(), 1);
3241        assert!(
3242            errs[0].contains("missing field 'age'"),
3243            "expected detail about missing field, got: {}",
3244            errs[0]
3245        );
3246    }
3247
3248    #[test]
3249    fn test_shape_mismatch_detail_wrong_type() {
3250        let errs = errors(
3251            r#"pipeline t(task) {
3252  let x: {name: string, age: int} = {name: 42, age: 10}
3253}"#,
3254        );
3255        assert_eq!(errs.len(), 1);
3256        assert!(
3257            errs[0].contains("field 'name' has type int, expected string"),
3258            "expected detail about wrong type, got: {}",
3259            errs[0]
3260        );
3261    }
3262
3263    // --- Match pattern type validation tests ---
3264
3265    #[test]
3266    fn test_match_pattern_string_against_int() {
3267        let warns = warnings(
3268            r#"pipeline t(task) {
3269  let x: int = 42
3270  match x {
3271    "hello" -> { log("bad") }
3272    42 -> { log("ok") }
3273  }
3274}"#,
3275        );
3276        let pattern_warns: Vec<_> = warns
3277            .iter()
3278            .filter(|w| w.contains("Match pattern type mismatch"))
3279            .collect();
3280        assert_eq!(pattern_warns.len(), 1);
3281        assert!(pattern_warns[0].contains("matching int against string literal"));
3282    }
3283
3284    #[test]
3285    fn test_match_pattern_int_against_string() {
3286        let warns = warnings(
3287            r#"pipeline t(task) {
3288  let x: string = "hello"
3289  match x {
3290    42 -> { log("bad") }
3291    "hello" -> { log("ok") }
3292  }
3293}"#,
3294        );
3295        let pattern_warns: Vec<_> = warns
3296            .iter()
3297            .filter(|w| w.contains("Match pattern type mismatch"))
3298            .collect();
3299        assert_eq!(pattern_warns.len(), 1);
3300        assert!(pattern_warns[0].contains("matching string against int literal"));
3301    }
3302
3303    #[test]
3304    fn test_match_pattern_bool_against_int() {
3305        let warns = warnings(
3306            r#"pipeline t(task) {
3307  let x: int = 42
3308  match x {
3309    true -> { log("bad") }
3310    42 -> { log("ok") }
3311  }
3312}"#,
3313        );
3314        let pattern_warns: Vec<_> = warns
3315            .iter()
3316            .filter(|w| w.contains("Match pattern type mismatch"))
3317            .collect();
3318        assert_eq!(pattern_warns.len(), 1);
3319        assert!(pattern_warns[0].contains("matching int against bool literal"));
3320    }
3321
3322    #[test]
3323    fn test_match_pattern_float_against_string() {
3324        let warns = warnings(
3325            r#"pipeline t(task) {
3326  let x: string = "hello"
3327  match x {
3328    3.14 -> { log("bad") }
3329    "hello" -> { log("ok") }
3330  }
3331}"#,
3332        );
3333        let pattern_warns: Vec<_> = warns
3334            .iter()
3335            .filter(|w| w.contains("Match pattern type mismatch"))
3336            .collect();
3337        assert_eq!(pattern_warns.len(), 1);
3338        assert!(pattern_warns[0].contains("matching string against float literal"));
3339    }
3340
3341    #[test]
3342    fn test_match_pattern_int_against_float_ok() {
3343        // int and float are compatible for match patterns
3344        let warns = warnings(
3345            r#"pipeline t(task) {
3346  let x: float = 3.14
3347  match x {
3348    42 -> { log("ok") }
3349    _ -> { log("default") }
3350  }
3351}"#,
3352        );
3353        let pattern_warns: Vec<_> = warns
3354            .iter()
3355            .filter(|w| w.contains("Match pattern type mismatch"))
3356            .collect();
3357        assert!(pattern_warns.is_empty());
3358    }
3359
3360    #[test]
3361    fn test_match_pattern_float_against_int_ok() {
3362        // float and int are compatible for match patterns
3363        let warns = warnings(
3364            r#"pipeline t(task) {
3365  let x: int = 42
3366  match x {
3367    3.14 -> { log("close") }
3368    _ -> { log("default") }
3369  }
3370}"#,
3371        );
3372        let pattern_warns: Vec<_> = warns
3373            .iter()
3374            .filter(|w| w.contains("Match pattern type mismatch"))
3375            .collect();
3376        assert!(pattern_warns.is_empty());
3377    }
3378
3379    #[test]
3380    fn test_match_pattern_correct_types_no_warning() {
3381        let warns = warnings(
3382            r#"pipeline t(task) {
3383  let x: int = 42
3384  match x {
3385    1 -> { log("one") }
3386    2 -> { log("two") }
3387    _ -> { log("other") }
3388  }
3389}"#,
3390        );
3391        let pattern_warns: Vec<_> = warns
3392            .iter()
3393            .filter(|w| w.contains("Match pattern type mismatch"))
3394            .collect();
3395        assert!(pattern_warns.is_empty());
3396    }
3397
3398    #[test]
3399    fn test_match_pattern_wildcard_no_warning() {
3400        let warns = warnings(
3401            r#"pipeline t(task) {
3402  let x: int = 42
3403  match x {
3404    _ -> { log("catch all") }
3405  }
3406}"#,
3407        );
3408        let pattern_warns: Vec<_> = warns
3409            .iter()
3410            .filter(|w| w.contains("Match pattern type mismatch"))
3411            .collect();
3412        assert!(pattern_warns.is_empty());
3413    }
3414
3415    #[test]
3416    fn test_match_pattern_untyped_no_warning() {
3417        // When value has no known type, no warning should be emitted
3418        let warns = warnings(
3419            r#"pipeline t(task) {
3420  let x = some_unknown_fn()
3421  match x {
3422    "hello" -> { log("string") }
3423    42 -> { log("int") }
3424  }
3425}"#,
3426        );
3427        let pattern_warns: Vec<_> = warns
3428            .iter()
3429            .filter(|w| w.contains("Match pattern type mismatch"))
3430            .collect();
3431        assert!(pattern_warns.is_empty());
3432    }
3433
3434    // --- Interface constraint type checking tests ---
3435
3436    fn iface_errors(source: &str) -> Vec<String> {
3437        errors(source)
3438            .into_iter()
3439            .filter(|message| message.contains("does not satisfy interface"))
3440            .collect()
3441    }
3442
3443    #[test]
3444    fn test_interface_constraint_return_type_mismatch() {
3445        let warns = iface_errors(
3446            r#"pipeline t(task) {
3447  interface Sizable {
3448    fn size(self) -> int
3449  }
3450  struct Box { width: int }
3451  impl Box {
3452    fn size(self) -> string { return "nope" }
3453  }
3454  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3455  measure(Box({width: 3}))
3456}"#,
3457        );
3458        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3459        assert!(
3460            warns[0].contains("method 'size' returns 'string', expected 'int'"),
3461            "unexpected message: {}",
3462            warns[0]
3463        );
3464    }
3465
3466    #[test]
3467    fn test_interface_constraint_param_type_mismatch() {
3468        let warns = iface_errors(
3469            r#"pipeline t(task) {
3470  interface Processor {
3471    fn process(self, x: int) -> string
3472  }
3473  struct MyProc { name: string }
3474  impl MyProc {
3475    fn process(self, x: string) -> string { return x }
3476  }
3477  fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
3478  run_proc(MyProc({name: "a"}))
3479}"#,
3480        );
3481        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3482        assert!(
3483            warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
3484            "unexpected message: {}",
3485            warns[0]
3486        );
3487    }
3488
3489    #[test]
3490    fn test_interface_constraint_missing_method() {
3491        let warns = iface_errors(
3492            r#"pipeline t(task) {
3493  interface Sizable {
3494    fn size(self) -> int
3495  }
3496  struct Box { width: int }
3497  impl Box {
3498    fn area(self) -> int { return self.width }
3499  }
3500  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3501  measure(Box({width: 3}))
3502}"#,
3503        );
3504        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3505        assert!(
3506            warns[0].contains("missing method 'size'"),
3507            "unexpected message: {}",
3508            warns[0]
3509        );
3510    }
3511
3512    #[test]
3513    fn test_interface_constraint_param_count_mismatch() {
3514        let warns = iface_errors(
3515            r#"pipeline t(task) {
3516  interface Doubler {
3517    fn double(self, x: int) -> int
3518  }
3519  struct Bad { v: int }
3520  impl Bad {
3521    fn double(self) -> int { return self.v * 2 }
3522  }
3523  fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
3524  run_double(Bad({v: 5}))
3525}"#,
3526        );
3527        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
3528        assert!(
3529            warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
3530            "unexpected message: {}",
3531            warns[0]
3532        );
3533    }
3534
3535    #[test]
3536    fn test_interface_constraint_satisfied() {
3537        let warns = iface_errors(
3538            r#"pipeline t(task) {
3539  interface Sizable {
3540    fn size(self) -> int
3541  }
3542  struct Box { width: int, height: int }
3543  impl Box {
3544    fn size(self) -> int { return self.width * self.height }
3545  }
3546  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3547  measure(Box({width: 3, height: 4}))
3548}"#,
3549        );
3550        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3551    }
3552
3553    #[test]
3554    fn test_interface_constraint_untyped_impl_compatible() {
3555        // Gradual typing: untyped impl return should not trigger warning
3556        let warns = iface_errors(
3557            r#"pipeline t(task) {
3558  interface Sizable {
3559    fn size(self) -> int
3560  }
3561  struct Box { width: int }
3562  impl Box {
3563    fn size(self) { return self.width }
3564  }
3565  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
3566  measure(Box({width: 3}))
3567}"#,
3568        );
3569        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3570    }
3571
3572    #[test]
3573    fn test_interface_constraint_int_float_covariance() {
3574        // int is compatible with float (covariance)
3575        let warns = iface_errors(
3576            r#"pipeline t(task) {
3577  interface Measurable {
3578    fn value(self) -> float
3579  }
3580  struct Gauge { v: int }
3581  impl Gauge {
3582    fn value(self) -> int { return self.v }
3583  }
3584  fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
3585  read_val(Gauge({v: 42}))
3586}"#,
3587        );
3588        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
3589    }
3590
3591    // --- Flow-sensitive type refinement tests ---
3592
3593    #[test]
3594    fn test_nil_narrowing_then_branch() {
3595        // Existing behavior: x != nil narrows to string in then-branch
3596        let errs = errors(
3597            r#"pipeline t(task) {
3598  fn greet(name: string | nil) {
3599    if name != nil {
3600      let s: string = name
3601    }
3602  }
3603}"#,
3604        );
3605        assert!(errs.is_empty(), "got: {:?}", errs);
3606    }
3607
3608    #[test]
3609    fn test_nil_narrowing_else_branch() {
3610        // NEW: x != nil narrows to nil in else-branch
3611        let errs = errors(
3612            r#"pipeline t(task) {
3613  fn check(x: string | nil) {
3614    if x != nil {
3615      let s: string = x
3616    } else {
3617      let n: nil = x
3618    }
3619  }
3620}"#,
3621        );
3622        assert!(errs.is_empty(), "got: {:?}", errs);
3623    }
3624
3625    #[test]
3626    fn test_nil_equality_narrows_both() {
3627        // x == nil narrows then to nil, else to non-nil
3628        let errs = errors(
3629            r#"pipeline t(task) {
3630  fn check(x: string | nil) {
3631    if x == nil {
3632      let n: nil = x
3633    } else {
3634      let s: string = x
3635    }
3636  }
3637}"#,
3638        );
3639        assert!(errs.is_empty(), "got: {:?}", errs);
3640    }
3641
3642    #[test]
3643    fn test_truthiness_narrowing() {
3644        // Bare identifier in condition removes nil
3645        let errs = errors(
3646            r#"pipeline t(task) {
3647  fn check(x: string | nil) {
3648    if x {
3649      let s: string = x
3650    }
3651  }
3652}"#,
3653        );
3654        assert!(errs.is_empty(), "got: {:?}", errs);
3655    }
3656
3657    #[test]
3658    fn test_negation_narrowing() {
3659        // !x swaps truthy/falsy
3660        let errs = errors(
3661            r#"pipeline t(task) {
3662  fn check(x: string | nil) {
3663    if !x {
3664      let n: nil = x
3665    } else {
3666      let s: string = x
3667    }
3668  }
3669}"#,
3670        );
3671        assert!(errs.is_empty(), "got: {:?}", errs);
3672    }
3673
3674    #[test]
3675    fn test_typeof_narrowing() {
3676        // type_of(x) == "string" narrows to string
3677        let errs = errors(
3678            r#"pipeline t(task) {
3679  fn check(x: string | int) {
3680    if type_of(x) == "string" {
3681      let s: string = x
3682    }
3683  }
3684}"#,
3685        );
3686        assert!(errs.is_empty(), "got: {:?}", errs);
3687    }
3688
3689    #[test]
3690    fn test_typeof_narrowing_else() {
3691        // else removes the tested type
3692        let errs = errors(
3693            r#"pipeline t(task) {
3694  fn check(x: string | int) {
3695    if type_of(x) == "string" {
3696      let s: string = x
3697    } else {
3698      let i: int = x
3699    }
3700  }
3701}"#,
3702        );
3703        assert!(errs.is_empty(), "got: {:?}", errs);
3704    }
3705
3706    #[test]
3707    fn test_typeof_neq_narrowing() {
3708        // type_of(x) != "string" removes string in then, narrows to string in else
3709        let errs = errors(
3710            r#"pipeline t(task) {
3711  fn check(x: string | int) {
3712    if type_of(x) != "string" {
3713      let i: int = x
3714    } else {
3715      let s: string = x
3716    }
3717  }
3718}"#,
3719        );
3720        assert!(errs.is_empty(), "got: {:?}", errs);
3721    }
3722
3723    #[test]
3724    fn test_and_combines_narrowing() {
3725        // && combines truthy refinements
3726        let errs = errors(
3727            r#"pipeline t(task) {
3728  fn check(x: string | int | nil) {
3729    if x != nil && type_of(x) == "string" {
3730      let s: string = x
3731    }
3732  }
3733}"#,
3734        );
3735        assert!(errs.is_empty(), "got: {:?}", errs);
3736    }
3737
3738    #[test]
3739    fn test_or_falsy_narrowing() {
3740        // || combines falsy refinements
3741        let errs = errors(
3742            r#"pipeline t(task) {
3743  fn check(x: string | nil, y: int | nil) {
3744    if x || y {
3745      // conservative: can't narrow
3746    } else {
3747      let xn: nil = x
3748      let yn: nil = y
3749    }
3750  }
3751}"#,
3752        );
3753        assert!(errs.is_empty(), "got: {:?}", errs);
3754    }
3755
3756    #[test]
3757    fn test_guard_narrows_outer_scope() {
3758        let errs = errors(
3759            r#"pipeline t(task) {
3760  fn check(x: string | nil) {
3761    guard x != nil else { return }
3762    let s: string = x
3763  }
3764}"#,
3765        );
3766        assert!(errs.is_empty(), "got: {:?}", errs);
3767    }
3768
3769    #[test]
3770    fn test_while_narrows_body() {
3771        let errs = errors(
3772            r#"pipeline t(task) {
3773  fn check(x: string | nil) {
3774    while x != nil {
3775      let s: string = x
3776      break
3777    }
3778  }
3779}"#,
3780        );
3781        assert!(errs.is_empty(), "got: {:?}", errs);
3782    }
3783
3784    #[test]
3785    fn test_early_return_narrows_after_if() {
3786        // if then-body returns, falsy refinements apply after
3787        let errs = errors(
3788            r#"pipeline t(task) {
3789  fn check(x: string | nil) -> string {
3790    if x == nil {
3791      return "default"
3792    }
3793    let s: string = x
3794    return s
3795  }
3796}"#,
3797        );
3798        assert!(errs.is_empty(), "got: {:?}", errs);
3799    }
3800
3801    #[test]
3802    fn test_early_throw_narrows_after_if() {
3803        let errs = errors(
3804            r#"pipeline t(task) {
3805  fn check(x: string | nil) {
3806    if x == nil {
3807      throw "missing"
3808    }
3809    let s: string = x
3810  }
3811}"#,
3812        );
3813        assert!(errs.is_empty(), "got: {:?}", errs);
3814    }
3815
3816    #[test]
3817    fn test_no_narrowing_unknown_type() {
3818        // Gradual typing: untyped vars don't get narrowed
3819        let errs = errors(
3820            r#"pipeline t(task) {
3821  fn check(x) {
3822    if x != nil {
3823      let s: string = x
3824    }
3825  }
3826}"#,
3827        );
3828        // No narrowing possible, so assigning untyped x to string should be fine
3829        // (gradual typing allows it)
3830        assert!(errs.is_empty(), "got: {:?}", errs);
3831    }
3832
3833    #[test]
3834    fn test_reassignment_invalidates_narrowing() {
3835        // After reassigning a narrowed var, the original type should be restored
3836        let errs = errors(
3837            r#"pipeline t(task) {
3838  fn check(x: string | nil) {
3839    var y: string | nil = x
3840    if y != nil {
3841      let s: string = y
3842      y = nil
3843      let s2: string = y
3844    }
3845  }
3846}"#,
3847        );
3848        // s2 should fail because y was reassigned, invalidating the narrowing
3849        assert_eq!(errs.len(), 1, "expected 1 error, got: {:?}", errs);
3850        assert!(
3851            errs[0].contains("Type mismatch"),
3852            "expected type mismatch, got: {}",
3853            errs[0]
3854        );
3855    }
3856
3857    #[test]
3858    fn test_let_immutable_warning() {
3859        let all = check_source(
3860            r#"pipeline t(task) {
3861  let x = 42
3862  x = 43
3863}"#,
3864        );
3865        let warnings: Vec<_> = all
3866            .iter()
3867            .filter(|d| d.severity == DiagnosticSeverity::Warning)
3868            .collect();
3869        assert!(
3870            warnings.iter().any(|w| w.message.contains("immutable")),
3871            "expected immutability warning, got: {:?}",
3872            warnings
3873        );
3874    }
3875
3876    #[test]
3877    fn test_nested_narrowing() {
3878        let errs = errors(
3879            r#"pipeline t(task) {
3880  fn check(x: string | int | nil) {
3881    if x != nil {
3882      if type_of(x) == "int" {
3883        let i: int = x
3884      }
3885    }
3886  }
3887}"#,
3888        );
3889        assert!(errs.is_empty(), "got: {:?}", errs);
3890    }
3891
3892    #[test]
3893    fn test_match_narrows_arms() {
3894        let errs = errors(
3895            r#"pipeline t(task) {
3896  fn check(x: string | int) {
3897    match x {
3898      "hello" -> {
3899        let s: string = x
3900      }
3901      42 -> {
3902        let i: int = x
3903      }
3904      _ -> {}
3905    }
3906  }
3907}"#,
3908        );
3909        assert!(errs.is_empty(), "got: {:?}", errs);
3910    }
3911
3912    #[test]
3913    fn test_has_narrows_optional_field() {
3914        let errs = errors(
3915            r#"pipeline t(task) {
3916  fn check(x: {name?: string, age: int}) {
3917    if x.has("name") {
3918      let n: {name: string, age: int} = x
3919    }
3920  }
3921}"#,
3922        );
3923        assert!(errs.is_empty(), "got: {:?}", errs);
3924    }
3925
3926    // -----------------------------------------------------------------------
3927    // Autofix tests
3928    // -----------------------------------------------------------------------
3929
3930    fn check_source_with_source(source: &str) -> Vec<TypeDiagnostic> {
3931        let mut lexer = Lexer::new(source);
3932        let tokens = lexer.tokenize().unwrap();
3933        let mut parser = Parser::new(tokens);
3934        let program = parser.parse().unwrap();
3935        TypeChecker::new().check_with_source(&program, source)
3936    }
3937
3938    #[test]
3939    fn test_fix_string_plus_int_literal() {
3940        let source = "pipeline t(task) {\n  let x = \"hello \" + 42\n  log(x)\n}";
3941        let diags = check_source_with_source(source);
3942        let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3943        assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
3944        let fix = fixable[0].fix.as_ref().unwrap();
3945        assert_eq!(fix.len(), 1);
3946        assert_eq!(fix[0].replacement, "\"hello ${42}\"");
3947    }
3948
3949    #[test]
3950    fn test_fix_int_plus_string_literal() {
3951        let source = "pipeline t(task) {\n  let x = 42 + \"hello\"\n  log(x)\n}";
3952        let diags = check_source_with_source(source);
3953        let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3954        assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
3955        let fix = fixable[0].fix.as_ref().unwrap();
3956        assert_eq!(fix[0].replacement, "\"${42}hello\"");
3957    }
3958
3959    #[test]
3960    fn test_fix_string_plus_variable() {
3961        let source = "pipeline t(task) {\n  let n: int = 5\n  let x = \"count: \" + n\n  log(x)\n}";
3962        let diags = check_source_with_source(source);
3963        let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3964        assert_eq!(fixable.len(), 1, "expected 1 fixable diagnostic");
3965        let fix = fixable[0].fix.as_ref().unwrap();
3966        assert_eq!(fix[0].replacement, "\"count: ${n}\"");
3967    }
3968
3969    #[test]
3970    fn test_no_fix_int_plus_int() {
3971        // int + float should error but no interpolation fix
3972        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}";
3973        let diags = check_source_with_source(source);
3974        let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3975        assert!(
3976            fixable.is_empty(),
3977            "no fix expected for numeric ops, got: {fixable:?}"
3978        );
3979    }
3980
3981    #[test]
3982    fn test_no_fix_without_source() {
3983        let source = "pipeline t(task) {\n  let x = \"hello \" + 42\n  log(x)\n}";
3984        let diags = check_source(source);
3985        let fixable: Vec<_> = diags.iter().filter(|d| d.fix.is_some()).collect();
3986        assert!(
3987            fixable.is_empty(),
3988            "without source, no fix should be generated"
3989        );
3990    }
3991
3992    // --- Union exhaustiveness tests ---
3993
3994    #[test]
3995    fn test_union_exhaustive_match_no_warning() {
3996        let warns = warnings(
3997            r#"pipeline t(task) {
3998  let x: string | int | nil = nil
3999  match x {
4000    "hello" -> { log("s") }
4001    42 -> { log("i") }
4002    nil -> { log("n") }
4003  }
4004}"#,
4005        );
4006        let union_warns: Vec<_> = warns
4007            .iter()
4008            .filter(|w| w.contains("Non-exhaustive match on union"))
4009            .collect();
4010        assert!(union_warns.is_empty());
4011    }
4012
4013    #[test]
4014    fn test_union_non_exhaustive_match_warning() {
4015        let warns = warnings(
4016            r#"pipeline t(task) {
4017  let x: string | int | nil = nil
4018  match x {
4019    "hello" -> { log("s") }
4020    42 -> { log("i") }
4021  }
4022}"#,
4023        );
4024        let union_warns: Vec<_> = warns
4025            .iter()
4026            .filter(|w| w.contains("Non-exhaustive match on union"))
4027            .collect();
4028        assert_eq!(union_warns.len(), 1);
4029        assert!(union_warns[0].contains("nil"));
4030    }
4031
4032    // --- Nil-coalescing type inference tests ---
4033
4034    #[test]
4035    fn test_nil_coalesce_non_union_preserves_left_type() {
4036        // When left is a known non-nil type, ?? should preserve it
4037        let errs = errors(
4038            r#"pipeline t(task) {
4039  let x: int = 42
4040  let y: int = x ?? 0
4041}"#,
4042        );
4043        assert!(errs.is_empty());
4044    }
4045
4046    #[test]
4047    fn test_nil_coalesce_nil_returns_right_type() {
4048        let errs = errors(
4049            r#"pipeline t(task) {
4050  let x: string = nil ?? "fallback"
4051}"#,
4052        );
4053        assert!(errs.is_empty());
4054    }
4055}