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