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