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