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