Skip to main content

harn_parser/
typechecker.rs

1use std::collections::BTreeMap;
2
3use crate::ast::*;
4use crate::builtin_signatures;
5use harn_lexer::Span;
6
7/// A diagnostic produced by the type checker.
8#[derive(Debug, Clone)]
9pub struct TypeDiagnostic {
10    pub message: String,
11    pub severity: DiagnosticSeverity,
12    pub span: Option<Span>,
13    pub help: Option<String>,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum DiagnosticSeverity {
18    Error,
19    Warning,
20}
21
22/// Inferred type of an expression. None means unknown/untyped (gradual typing).
23type InferredType = Option<TypeExpr>;
24
25/// Scope for tracking variable types.
26#[derive(Debug, Clone)]
27struct TypeScope {
28    /// Variable name → inferred type.
29    vars: BTreeMap<String, InferredType>,
30    /// Function name → (param types, return type).
31    functions: BTreeMap<String, FnSignature>,
32    /// Named type aliases.
33    type_aliases: BTreeMap<String, TypeExpr>,
34    /// Enum declarations: name → variant names.
35    enums: BTreeMap<String, Vec<String>>,
36    /// Interface declarations: name → method signatures.
37    interfaces: BTreeMap<String, Vec<InterfaceMethod>>,
38    /// Struct declarations: name → field types.
39    structs: BTreeMap<String, Vec<(String, InferredType)>>,
40    /// Impl block methods: type_name → method signatures.
41    impl_methods: BTreeMap<String, Vec<ImplMethodSig>>,
42    /// Generic type parameter names in scope (treated as compatible with any type).
43    generic_type_params: std::collections::BTreeSet<String>,
44    /// Where-clause constraints: type_param → interface_bound.
45    /// Used for definition-site checking of generic function bodies.
46    where_constraints: BTreeMap<String, String>,
47    parent: Option<Box<TypeScope>>,
48}
49
50/// Method signature extracted from an impl block (for interface checking).
51#[derive(Debug, Clone)]
52struct ImplMethodSig {
53    name: String,
54    /// Number of parameters excluding `self`.
55    param_count: usize,
56    /// Parameter types (excluding `self`), None means untyped.
57    param_types: Vec<Option<TypeExpr>>,
58    /// Return type, None means untyped.
59    return_type: Option<TypeExpr>,
60}
61
62#[derive(Debug, Clone)]
63struct FnSignature {
64    params: Vec<(String, InferredType)>,
65    return_type: InferredType,
66    /// Generic type parameter names declared on the function.
67    type_param_names: Vec<String>,
68    /// Number of required parameters (those without defaults).
69    required_params: usize,
70    /// Where-clause constraints: (type_param_name, interface_bound).
71    where_clauses: Vec<(String, String)>,
72}
73
74impl TypeScope {
75    fn new() -> Self {
76        Self {
77            vars: BTreeMap::new(),
78            functions: BTreeMap::new(),
79            type_aliases: BTreeMap::new(),
80            enums: BTreeMap::new(),
81            interfaces: BTreeMap::new(),
82            structs: BTreeMap::new(),
83            impl_methods: BTreeMap::new(),
84            generic_type_params: std::collections::BTreeSet::new(),
85            where_constraints: BTreeMap::new(),
86            parent: None,
87        }
88    }
89
90    fn child(&self) -> Self {
91        Self {
92            vars: BTreeMap::new(),
93            functions: BTreeMap::new(),
94            type_aliases: BTreeMap::new(),
95            enums: BTreeMap::new(),
96            interfaces: BTreeMap::new(),
97            structs: BTreeMap::new(),
98            impl_methods: BTreeMap::new(),
99            generic_type_params: std::collections::BTreeSet::new(),
100            where_constraints: BTreeMap::new(),
101            parent: Some(Box::new(self.clone())),
102        }
103    }
104
105    fn get_var(&self, name: &str) -> Option<&InferredType> {
106        self.vars
107            .get(name)
108            .or_else(|| self.parent.as_ref()?.get_var(name))
109    }
110
111    fn get_fn(&self, name: &str) -> Option<&FnSignature> {
112        self.functions
113            .get(name)
114            .or_else(|| self.parent.as_ref()?.get_fn(name))
115    }
116
117    fn resolve_type(&self, name: &str) -> Option<&TypeExpr> {
118        self.type_aliases
119            .get(name)
120            .or_else(|| self.parent.as_ref()?.resolve_type(name))
121    }
122
123    fn is_generic_type_param(&self, name: &str) -> bool {
124        self.generic_type_params.contains(name)
125            || self
126                .parent
127                .as_ref()
128                .is_some_and(|p| p.is_generic_type_param(name))
129    }
130
131    fn get_where_constraint(&self, type_param: &str) -> Option<&str> {
132        self.where_constraints
133            .get(type_param)
134            .map(|s| s.as_str())
135            .or_else(|| {
136                self.parent
137                    .as_ref()
138                    .and_then(|p| p.get_where_constraint(type_param))
139            })
140    }
141
142    fn get_enum(&self, name: &str) -> Option<&Vec<String>> {
143        self.enums
144            .get(name)
145            .or_else(|| self.parent.as_ref()?.get_enum(name))
146    }
147
148    fn get_interface(&self, name: &str) -> Option<&Vec<InterfaceMethod>> {
149        self.interfaces
150            .get(name)
151            .or_else(|| self.parent.as_ref()?.get_interface(name))
152    }
153
154    fn get_struct(&self, name: &str) -> Option<&Vec<(String, InferredType)>> {
155        self.structs
156            .get(name)
157            .or_else(|| self.parent.as_ref()?.get_struct(name))
158    }
159
160    fn get_impl_methods(&self, name: &str) -> Option<&Vec<ImplMethodSig>> {
161        self.impl_methods
162            .get(name)
163            .or_else(|| self.parent.as_ref()?.get_impl_methods(name))
164    }
165
166    fn define_var(&mut self, name: &str, ty: InferredType) {
167        self.vars.insert(name.to_string(), ty);
168    }
169
170    fn define_fn(&mut self, name: &str, sig: FnSignature) {
171        self.functions.insert(name.to_string(), sig);
172    }
173}
174
175/// Known return types for builtin functions. Delegates to the shared
176/// [`builtin_signatures`] registry — see that module for the full table.
177fn builtin_return_type(name: &str) -> InferredType {
178    builtin_signatures::builtin_return_type(name)
179}
180
181/// Check if a name is a known builtin. Delegates to the shared
182/// [`builtin_signatures`] registry.
183fn is_builtin(name: &str) -> bool {
184    builtin_signatures::is_builtin(name)
185}
186
187/// The static type checker.
188pub struct TypeChecker {
189    diagnostics: Vec<TypeDiagnostic>,
190    scope: TypeScope,
191}
192
193impl TypeChecker {
194    pub fn new() -> Self {
195        Self {
196            diagnostics: Vec::new(),
197            scope: TypeScope::new(),
198        }
199    }
200
201    /// Check a program and return diagnostics.
202    pub fn check(mut self, program: &[SNode]) -> Vec<TypeDiagnostic> {
203        // First pass: register type and enum declarations into root scope
204        Self::register_declarations_into(&mut self.scope, program);
205
206        // Also scan pipeline bodies for declarations
207        for snode in program {
208            if let Node::Pipeline { body, .. } = &snode.node {
209                Self::register_declarations_into(&mut self.scope, body);
210            }
211        }
212
213        // Check each top-level node
214        for snode in program {
215            match &snode.node {
216                Node::Pipeline { params, body, .. } => {
217                    let mut child = self.scope.child();
218                    for p in params {
219                        child.define_var(p, None);
220                    }
221                    self.check_block(body, &mut child);
222                }
223                Node::FnDecl {
224                    name,
225                    type_params,
226                    params,
227                    return_type,
228                    where_clauses,
229                    body,
230                    ..
231                } => {
232                    let required_params =
233                        params.iter().filter(|p| p.default_value.is_none()).count();
234                    let sig = FnSignature {
235                        params: params
236                            .iter()
237                            .map(|p| (p.name.clone(), p.type_expr.clone()))
238                            .collect(),
239                        return_type: return_type.clone(),
240                        type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
241                        required_params,
242                        where_clauses: where_clauses
243                            .iter()
244                            .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
245                            .collect(),
246                    };
247                    self.scope.define_fn(name, sig);
248                    self.check_fn_body(type_params, params, return_type, body, where_clauses);
249                }
250                _ => {
251                    let mut scope = self.scope.clone();
252                    self.check_node(snode, &mut scope);
253                    // Merge any new definitions back into the top-level scope
254                    for (name, ty) in scope.vars {
255                        self.scope.vars.entry(name).or_insert(ty);
256                    }
257                }
258            }
259        }
260
261        self.diagnostics
262    }
263
264    /// Register type, enum, interface, and struct declarations from AST nodes into a scope.
265    fn register_declarations_into(scope: &mut TypeScope, nodes: &[SNode]) {
266        for snode in nodes {
267            match &snode.node {
268                Node::TypeDecl { name, type_expr } => {
269                    scope.type_aliases.insert(name.clone(), type_expr.clone());
270                }
271                Node::EnumDecl { name, variants, .. } => {
272                    let variant_names: Vec<String> =
273                        variants.iter().map(|v| v.name.clone()).collect();
274                    scope.enums.insert(name.clone(), variant_names);
275                }
276                Node::InterfaceDecl { name, methods, .. } => {
277                    scope.interfaces.insert(name.clone(), methods.clone());
278                }
279                Node::StructDecl { name, fields, .. } => {
280                    let field_types: Vec<(String, InferredType)> = fields
281                        .iter()
282                        .map(|f| (f.name.clone(), f.type_expr.clone()))
283                        .collect();
284                    scope.structs.insert(name.clone(), field_types);
285                }
286                Node::ImplBlock {
287                    type_name, methods, ..
288                } => {
289                    let sigs: Vec<ImplMethodSig> = methods
290                        .iter()
291                        .filter_map(|m| {
292                            if let Node::FnDecl {
293                                name,
294                                params,
295                                return_type,
296                                ..
297                            } = &m.node
298                            {
299                                let non_self: Vec<_> =
300                                    params.iter().filter(|p| p.name != "self").collect();
301                                let param_count = non_self.len();
302                                let param_types: Vec<Option<TypeExpr>> =
303                                    non_self.iter().map(|p| p.type_expr.clone()).collect();
304                                Some(ImplMethodSig {
305                                    name: name.clone(),
306                                    param_count,
307                                    param_types,
308                                    return_type: return_type.clone(),
309                                })
310                            } else {
311                                None
312                            }
313                        })
314                        .collect();
315                    scope.impl_methods.insert(type_name.clone(), sigs);
316                }
317                _ => {}
318            }
319        }
320    }
321
322    fn check_block(&mut self, stmts: &[SNode], scope: &mut TypeScope) {
323        for stmt in stmts {
324            self.check_node(stmt, scope);
325        }
326    }
327
328    /// Define variables from a destructuring pattern in the given scope (as unknown type).
329    fn define_pattern_vars(pattern: &BindingPattern, scope: &mut TypeScope) {
330        match pattern {
331            BindingPattern::Identifier(name) => {
332                scope.define_var(name, None);
333            }
334            BindingPattern::Dict(fields) => {
335                for field in fields {
336                    let name = field.alias.as_deref().unwrap_or(&field.key);
337                    scope.define_var(name, None);
338                }
339            }
340            BindingPattern::List(elements) => {
341                for elem in elements {
342                    scope.define_var(&elem.name, None);
343                }
344            }
345        }
346    }
347
348    fn check_node(&mut self, snode: &SNode, scope: &mut TypeScope) {
349        let span = snode.span;
350        match &snode.node {
351            Node::LetBinding {
352                pattern,
353                type_ann,
354                value,
355            } => {
356                let inferred = self.infer_type(value, scope);
357                if let BindingPattern::Identifier(name) = pattern {
358                    if let Some(expected) = type_ann {
359                        if let Some(actual) = &inferred {
360                            if !self.types_compatible(expected, actual, scope) {
361                                let mut msg = format!(
362                                    "Type mismatch: '{}' declared as {}, but assigned {}",
363                                    name,
364                                    format_type(expected),
365                                    format_type(actual)
366                                );
367                                if let Some(detail) = shape_mismatch_detail(expected, actual) {
368                                    msg.push_str(&format!(" ({})", detail));
369                                }
370                                self.error_at(msg, span);
371                            }
372                        }
373                    }
374                    let ty = type_ann.clone().or(inferred);
375                    scope.define_var(name, ty);
376                } else {
377                    Self::define_pattern_vars(pattern, scope);
378                }
379            }
380
381            Node::VarBinding {
382                pattern,
383                type_ann,
384                value,
385            } => {
386                let inferred = self.infer_type(value, scope);
387                if let BindingPattern::Identifier(name) = pattern {
388                    if let Some(expected) = type_ann {
389                        if let Some(actual) = &inferred {
390                            if !self.types_compatible(expected, actual, scope) {
391                                let mut msg = format!(
392                                    "Type mismatch: '{}' declared as {}, but assigned {}",
393                                    name,
394                                    format_type(expected),
395                                    format_type(actual)
396                                );
397                                if let Some(detail) = shape_mismatch_detail(expected, actual) {
398                                    msg.push_str(&format!(" ({})", detail));
399                                }
400                                self.error_at(msg, span);
401                            }
402                        }
403                    }
404                    let ty = type_ann.clone().or(inferred);
405                    scope.define_var(name, ty);
406                } else {
407                    Self::define_pattern_vars(pattern, scope);
408                }
409            }
410
411            Node::FnDecl {
412                name,
413                type_params,
414                params,
415                return_type,
416                where_clauses,
417                body,
418                ..
419            } => {
420                let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
421                let sig = FnSignature {
422                    params: params
423                        .iter()
424                        .map(|p| (p.name.clone(), p.type_expr.clone()))
425                        .collect(),
426                    return_type: return_type.clone(),
427                    type_param_names: type_params.iter().map(|tp| tp.name.clone()).collect(),
428                    required_params,
429                    where_clauses: where_clauses
430                        .iter()
431                        .map(|wc| (wc.type_name.clone(), wc.bound.clone()))
432                        .collect(),
433                };
434                scope.define_fn(name, sig.clone());
435                scope.define_var(name, None);
436                self.check_fn_body(type_params, params, return_type, body, where_clauses);
437            }
438
439            Node::ToolDecl {
440                name,
441                params,
442                return_type,
443                body,
444                ..
445            } => {
446                // Register the tool like a function for type checking purposes
447                let required_params = params.iter().filter(|p| p.default_value.is_none()).count();
448                let sig = FnSignature {
449                    params: params
450                        .iter()
451                        .map(|p| (p.name.clone(), p.type_expr.clone()))
452                        .collect(),
453                    return_type: return_type.clone(),
454                    type_param_names: Vec::new(),
455                    required_params,
456                    where_clauses: Vec::new(),
457                };
458                scope.define_fn(name, sig);
459                scope.define_var(name, None);
460                self.check_fn_body(&[], params, return_type, body, &[]);
461            }
462
463            Node::FunctionCall { name, args } => {
464                self.check_call(name, args, scope, span);
465            }
466
467            Node::IfElse {
468                condition,
469                then_body,
470                else_body,
471            } => {
472                self.check_node(condition, scope);
473                let mut then_scope = scope.child();
474                // Narrow union types after nil checks: if x != nil, narrow x
475                if let Some((var_name, narrowed)) = Self::extract_nil_narrowing(condition, scope) {
476                    then_scope.define_var(&var_name, narrowed);
477                }
478                self.check_block(then_body, &mut then_scope);
479                if let Some(else_body) = else_body {
480                    let mut else_scope = scope.child();
481                    self.check_block(else_body, &mut else_scope);
482                }
483            }
484
485            Node::ForIn {
486                pattern,
487                iterable,
488                body,
489            } => {
490                self.check_node(iterable, scope);
491                let mut loop_scope = scope.child();
492                if let BindingPattern::Identifier(variable) = pattern {
493                    // Infer loop variable type from iterable
494                    let elem_type = match self.infer_type(iterable, scope) {
495                        Some(TypeExpr::List(inner)) => Some(*inner),
496                        Some(TypeExpr::Named(n)) if n == "string" => {
497                            Some(TypeExpr::Named("string".into()))
498                        }
499                        _ => None,
500                    };
501                    loop_scope.define_var(variable, elem_type);
502                } else {
503                    Self::define_pattern_vars(pattern, &mut loop_scope);
504                }
505                self.check_block(body, &mut loop_scope);
506            }
507
508            Node::WhileLoop { condition, body } => {
509                self.check_node(condition, scope);
510                let mut loop_scope = scope.child();
511                self.check_block(body, &mut loop_scope);
512            }
513
514            Node::RequireStmt { condition, message } => {
515                self.check_node(condition, scope);
516                if let Some(message) = message {
517                    self.check_node(message, scope);
518                }
519            }
520
521            Node::TryCatch {
522                body,
523                error_var,
524                catch_body,
525                finally_body,
526                ..
527            } => {
528                let mut try_scope = scope.child();
529                self.check_block(body, &mut try_scope);
530                let mut catch_scope = scope.child();
531                if let Some(var) = error_var {
532                    catch_scope.define_var(var, None);
533                }
534                self.check_block(catch_body, &mut catch_scope);
535                if let Some(fb) = finally_body {
536                    let mut finally_scope = scope.child();
537                    self.check_block(fb, &mut finally_scope);
538                }
539            }
540
541            Node::TryExpr { body } => {
542                let mut try_scope = scope.child();
543                self.check_block(body, &mut try_scope);
544            }
545
546            Node::ReturnStmt {
547                value: Some(val), ..
548            } => {
549                self.check_node(val, scope);
550            }
551
552            Node::Assignment {
553                target, value, op, ..
554            } => {
555                self.check_node(value, scope);
556                if let Node::Identifier(name) = &target.node {
557                    if let Some(Some(var_type)) = scope.get_var(name) {
558                        let value_type = self.infer_type(value, scope);
559                        let assigned = if let Some(op) = op {
560                            let var_inferred = scope.get_var(name).cloned().flatten();
561                            infer_binary_op_type(op, &var_inferred, &value_type)
562                        } else {
563                            value_type
564                        };
565                        if let Some(actual) = &assigned {
566                            if !self.types_compatible(var_type, actual, scope) {
567                                self.error_at(
568                                    format!(
569                                        "Type mismatch: cannot assign {} to '{}' (declared as {})",
570                                        format_type(actual),
571                                        name,
572                                        format_type(var_type)
573                                    ),
574                                    span,
575                                );
576                            }
577                        }
578                    }
579                }
580            }
581
582            Node::TypeDecl { name, type_expr } => {
583                scope.type_aliases.insert(name.clone(), type_expr.clone());
584            }
585
586            Node::EnumDecl { name, variants, .. } => {
587                let variant_names: Vec<String> = variants.iter().map(|v| v.name.clone()).collect();
588                scope.enums.insert(name.clone(), variant_names);
589            }
590
591            Node::StructDecl { name, fields, .. } => {
592                let field_types: Vec<(String, InferredType)> = fields
593                    .iter()
594                    .map(|f| (f.name.clone(), f.type_expr.clone()))
595                    .collect();
596                scope.structs.insert(name.clone(), field_types);
597            }
598
599            Node::InterfaceDecl { name, methods, .. } => {
600                scope.interfaces.insert(name.clone(), methods.clone());
601            }
602
603            Node::ImplBlock {
604                type_name, methods, ..
605            } => {
606                // Register impl methods for interface satisfaction checking
607                let sigs: Vec<ImplMethodSig> = methods
608                    .iter()
609                    .filter_map(|m| {
610                        if let Node::FnDecl {
611                            name,
612                            params,
613                            return_type,
614                            ..
615                        } = &m.node
616                        {
617                            let non_self: Vec<_> =
618                                params.iter().filter(|p| p.name != "self").collect();
619                            let param_count = non_self.len();
620                            let param_types: Vec<Option<TypeExpr>> =
621                                non_self.iter().map(|p| p.type_expr.clone()).collect();
622                            Some(ImplMethodSig {
623                                name: name.clone(),
624                                param_count,
625                                param_types,
626                                return_type: return_type.clone(),
627                            })
628                        } else {
629                            None
630                        }
631                    })
632                    .collect();
633                scope.impl_methods.insert(type_name.clone(), sigs);
634                for method_sn in methods {
635                    self.check_node(method_sn, scope);
636                }
637            }
638
639            Node::TryOperator { operand } => {
640                self.check_node(operand, scope);
641            }
642
643            Node::MatchExpr { value, arms } => {
644                self.check_node(value, scope);
645                let value_type = self.infer_type(value, scope);
646                for arm in arms {
647                    self.check_node(&arm.pattern, scope);
648                    // Check for incompatible literal pattern types
649                    if let Some(ref vt) = value_type {
650                        let value_type_name = format_type(vt);
651                        let mismatch = match &arm.pattern.node {
652                            Node::StringLiteral(_) => {
653                                !self.types_compatible(vt, &TypeExpr::Named("string".into()), scope)
654                            }
655                            Node::IntLiteral(_) => {
656                                !self.types_compatible(vt, &TypeExpr::Named("int".into()), scope)
657                                    && !self.types_compatible(
658                                        vt,
659                                        &TypeExpr::Named("float".into()),
660                                        scope,
661                                    )
662                            }
663                            Node::FloatLiteral(_) => {
664                                !self.types_compatible(vt, &TypeExpr::Named("float".into()), scope)
665                                    && !self.types_compatible(
666                                        vt,
667                                        &TypeExpr::Named("int".into()),
668                                        scope,
669                                    )
670                            }
671                            Node::BoolLiteral(_) => {
672                                !self.types_compatible(vt, &TypeExpr::Named("bool".into()), scope)
673                            }
674                            _ => false,
675                        };
676                        if mismatch {
677                            let pattern_type = match &arm.pattern.node {
678                                Node::StringLiteral(_) => "string",
679                                Node::IntLiteral(_) => "int",
680                                Node::FloatLiteral(_) => "float",
681                                Node::BoolLiteral(_) => "bool",
682                                _ => unreachable!(),
683                            };
684                            self.warning_at(
685                                format!(
686                                    "Match pattern type mismatch: matching {} against {} literal",
687                                    value_type_name, pattern_type
688                                ),
689                                arm.pattern.span,
690                            );
691                        }
692                    }
693                    let mut arm_scope = scope.child();
694                    self.check_block(&arm.body, &mut arm_scope);
695                }
696                self.check_match_exhaustiveness(value, arms, scope, span);
697            }
698
699            // Recurse into nested expressions + validate binary op types
700            Node::BinaryOp { op, left, right } => {
701                self.check_node(left, scope);
702                self.check_node(right, scope);
703                // Validate operator/type compatibility
704                let lt = self.infer_type(left, scope);
705                let rt = self.infer_type(right, scope);
706                if let (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) = (&lt, &rt) {
707                    match op.as_str() {
708                        "-" | "*" | "/" | "%" => {
709                            let numeric = ["int", "float"];
710                            if !numeric.contains(&l.as_str()) || !numeric.contains(&r.as_str()) {
711                                self.warning_at(
712                                    format!(
713                                        "Operator '{op}' may not be valid for types {} and {}",
714                                        l, r
715                                    ),
716                                    span,
717                                );
718                            }
719                        }
720                        "+" => {
721                            // + is valid for int, float, string, list, dict
722                            let valid = ["int", "float", "string", "list", "dict"];
723                            if !valid.contains(&l.as_str()) && !valid.contains(&r.as_str()) {
724                                self.warning_at(
725                                    format!(
726                                        "Operator '+' may not be valid for types {} and {}",
727                                        l, r
728                                    ),
729                                    span,
730                                );
731                            }
732                        }
733                        _ => {}
734                    }
735                }
736            }
737            Node::UnaryOp { operand, .. } => {
738                self.check_node(operand, scope);
739            }
740            Node::MethodCall {
741                object,
742                method,
743                args,
744                ..
745            }
746            | Node::OptionalMethodCall {
747                object,
748                method,
749                args,
750                ..
751            } => {
752                self.check_node(object, scope);
753                for arg in args {
754                    self.check_node(arg, scope);
755                }
756                // Definition-site generic checking: if the object's type is a
757                // constrained generic param (where T: Interface), verify the
758                // method exists in the bound interface.
759                if let Some(TypeExpr::Named(type_name)) = self.infer_type(object, scope) {
760                    if scope.is_generic_type_param(&type_name) {
761                        if let Some(iface_name) = scope.get_where_constraint(&type_name) {
762                            if let Some(iface_methods) = scope.get_interface(iface_name) {
763                                let has_method = iface_methods.iter().any(|m| m.name == *method);
764                                if !has_method {
765                                    self.warning_at(
766                                        format!(
767                                            "Method '{}' not found in interface '{}' (constraint on '{}')",
768                                            method, iface_name, type_name
769                                        ),
770                                        span,
771                                    );
772                                }
773                            }
774                        }
775                    }
776                }
777            }
778            Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
779                self.check_node(object, scope);
780            }
781            Node::SubscriptAccess { object, index } => {
782                self.check_node(object, scope);
783                self.check_node(index, scope);
784            }
785            Node::SliceAccess { object, start, end } => {
786                self.check_node(object, scope);
787                if let Some(s) = start {
788                    self.check_node(s, scope);
789                }
790                if let Some(e) = end {
791                    self.check_node(e, scope);
792                }
793            }
794
795            // --- Compound nodes: recurse into children ---
796            Node::Ternary {
797                condition,
798                true_expr,
799                false_expr,
800            } => {
801                self.check_node(condition, scope);
802                self.check_node(true_expr, scope);
803                self.check_node(false_expr, scope);
804            }
805
806            Node::ThrowStmt { value } => {
807                self.check_node(value, scope);
808            }
809
810            Node::GuardStmt {
811                condition,
812                else_body,
813            } => {
814                self.check_node(condition, scope);
815                let mut else_scope = scope.child();
816                self.check_block(else_body, &mut else_scope);
817            }
818
819            Node::SpawnExpr { body } => {
820                let mut spawn_scope = scope.child();
821                self.check_block(body, &mut spawn_scope);
822            }
823
824            Node::Parallel {
825                count,
826                variable,
827                body,
828            } => {
829                self.check_node(count, scope);
830                let mut par_scope = scope.child();
831                if let Some(var) = variable {
832                    par_scope.define_var(var, Some(TypeExpr::Named("int".into())));
833                }
834                self.check_block(body, &mut par_scope);
835            }
836
837            Node::ParallelMap {
838                list,
839                variable,
840                body,
841            }
842            | Node::ParallelSettle {
843                list,
844                variable,
845                body,
846            } => {
847                self.check_node(list, scope);
848                let mut par_scope = scope.child();
849                let elem_type = match self.infer_type(list, scope) {
850                    Some(TypeExpr::List(inner)) => Some(*inner),
851                    _ => None,
852                };
853                par_scope.define_var(variable, elem_type);
854                self.check_block(body, &mut par_scope);
855            }
856
857            Node::SelectExpr {
858                cases,
859                timeout,
860                default_body,
861            } => {
862                for case in cases {
863                    self.check_node(&case.channel, scope);
864                    let mut case_scope = scope.child();
865                    case_scope.define_var(&case.variable, None);
866                    self.check_block(&case.body, &mut case_scope);
867                }
868                if let Some((dur, body)) = timeout {
869                    self.check_node(dur, scope);
870                    let mut timeout_scope = scope.child();
871                    self.check_block(body, &mut timeout_scope);
872                }
873                if let Some(body) = default_body {
874                    let mut default_scope = scope.child();
875                    self.check_block(body, &mut default_scope);
876                }
877            }
878
879            Node::DeadlineBlock { duration, body } => {
880                self.check_node(duration, scope);
881                let mut block_scope = scope.child();
882                self.check_block(body, &mut block_scope);
883            }
884
885            Node::MutexBlock { body } => {
886                let mut block_scope = scope.child();
887                self.check_block(body, &mut block_scope);
888            }
889
890            Node::Retry { count, body } => {
891                self.check_node(count, scope);
892                let mut retry_scope = scope.child();
893                self.check_block(body, &mut retry_scope);
894            }
895
896            Node::Closure { params, body, .. } => {
897                let mut closure_scope = scope.child();
898                for p in params {
899                    closure_scope.define_var(&p.name, p.type_expr.clone());
900                }
901                self.check_block(body, &mut closure_scope);
902            }
903
904            Node::ListLiteral(elements) => {
905                for elem in elements {
906                    self.check_node(elem, scope);
907                }
908            }
909
910            Node::DictLiteral(entries) | Node::AskExpr { fields: entries } => {
911                for entry in entries {
912                    self.check_node(&entry.key, scope);
913                    self.check_node(&entry.value, scope);
914                }
915            }
916
917            Node::RangeExpr { start, end, .. } => {
918                self.check_node(start, scope);
919                self.check_node(end, scope);
920            }
921
922            Node::Spread(inner) => {
923                self.check_node(inner, scope);
924            }
925
926            Node::Block(stmts) => {
927                let mut block_scope = scope.child();
928                self.check_block(stmts, &mut block_scope);
929            }
930
931            Node::YieldExpr { value } => {
932                if let Some(v) = value {
933                    self.check_node(v, scope);
934                }
935            }
936
937            // --- Struct construction: validate fields against declaration ---
938            Node::StructConstruct {
939                struct_name,
940                fields,
941            } => {
942                for entry in fields {
943                    self.check_node(&entry.key, scope);
944                    self.check_node(&entry.value, scope);
945                }
946                if let Some(declared_fields) = scope.get_struct(struct_name).cloned() {
947                    // Warn on unknown fields
948                    for entry in fields {
949                        if let Node::StringLiteral(key) | Node::Identifier(key) = &entry.key.node {
950                            if !declared_fields.iter().any(|(name, _)| name == key) {
951                                self.warning_at(
952                                    format!("Unknown field '{}' in struct '{}'", key, struct_name),
953                                    entry.key.span,
954                                );
955                            }
956                        }
957                    }
958                    // Warn on missing required fields
959                    let provided: Vec<String> = fields
960                        .iter()
961                        .filter_map(|e| match &e.key.node {
962                            Node::StringLiteral(k) | Node::Identifier(k) => Some(k.clone()),
963                            _ => None,
964                        })
965                        .collect();
966                    for (name, _) in &declared_fields {
967                        if !provided.contains(name) {
968                            self.warning_at(
969                                format!(
970                                    "Missing field '{}' in struct '{}' construction",
971                                    name, struct_name
972                                ),
973                                span,
974                            );
975                        }
976                    }
977                }
978            }
979
980            // --- Enum construction: validate variant exists ---
981            Node::EnumConstruct {
982                enum_name,
983                variant,
984                args,
985            } => {
986                for arg in args {
987                    self.check_node(arg, scope);
988                }
989                if let Some(variants) = scope.get_enum(enum_name) {
990                    if !variants.contains(variant) {
991                        self.warning_at(
992                            format!("Unknown variant '{}' in enum '{}'", variant, enum_name),
993                            span,
994                        );
995                    }
996                }
997            }
998
999            // --- InterpolatedString: segments are lexer-level, no SNode children ---
1000            Node::InterpolatedString(_) => {}
1001
1002            // --- Terminals: no children to check ---
1003            Node::StringLiteral(_)
1004            | Node::IntLiteral(_)
1005            | Node::FloatLiteral(_)
1006            | Node::BoolLiteral(_)
1007            | Node::NilLiteral
1008            | Node::Identifier(_)
1009            | Node::DurationLiteral(_)
1010            | Node::BreakStmt
1011            | Node::ContinueStmt
1012            | Node::ReturnStmt { value: None }
1013            | Node::ImportDecl { .. }
1014            | Node::SelectiveImport { .. } => {}
1015
1016            // Declarations already handled above; catch remaining variants
1017            // that have no meaningful type-check behavior.
1018            Node::Pipeline { body, .. } | Node::OverrideDecl { body, .. } => {
1019                let mut decl_scope = scope.child();
1020                self.check_block(body, &mut decl_scope);
1021            }
1022        }
1023    }
1024
1025    fn check_fn_body(
1026        &mut self,
1027        type_params: &[TypeParam],
1028        params: &[TypedParam],
1029        return_type: &Option<TypeExpr>,
1030        body: &[SNode],
1031        where_clauses: &[WhereClause],
1032    ) {
1033        let mut fn_scope = self.scope.child();
1034        // Register generic type parameters so they are treated as compatible
1035        // with any concrete type during type checking.
1036        for tp in type_params {
1037            fn_scope.generic_type_params.insert(tp.name.clone());
1038        }
1039        // Store where-clause constraints for definition-site checking
1040        for wc in where_clauses {
1041            fn_scope
1042                .where_constraints
1043                .insert(wc.type_name.clone(), wc.bound.clone());
1044        }
1045        for param in params {
1046            fn_scope.define_var(&param.name, param.type_expr.clone());
1047            if let Some(default) = &param.default_value {
1048                self.check_node(default, &mut fn_scope);
1049            }
1050        }
1051        self.check_block(body, &mut fn_scope);
1052
1053        // Check return statements against declared return type
1054        if let Some(ret_type) = return_type {
1055            for stmt in body {
1056                self.check_return_type(stmt, ret_type, &fn_scope);
1057            }
1058        }
1059    }
1060
1061    fn check_return_type(&mut self, snode: &SNode, expected: &TypeExpr, scope: &TypeScope) {
1062        let span = snode.span;
1063        match &snode.node {
1064            Node::ReturnStmt { value: Some(val) } => {
1065                let inferred = self.infer_type(val, scope);
1066                if let Some(actual) = &inferred {
1067                    if !self.types_compatible(expected, actual, scope) {
1068                        self.error_at(
1069                            format!(
1070                                "Return type mismatch: expected {}, got {}",
1071                                format_type(expected),
1072                                format_type(actual)
1073                            ),
1074                            span,
1075                        );
1076                    }
1077                }
1078            }
1079            Node::IfElse {
1080                then_body,
1081                else_body,
1082                ..
1083            } => {
1084                for stmt in then_body {
1085                    self.check_return_type(stmt, expected, scope);
1086                }
1087                if let Some(else_body) = else_body {
1088                    for stmt in else_body {
1089                        self.check_return_type(stmt, expected, scope);
1090                    }
1091                }
1092            }
1093            _ => {}
1094        }
1095    }
1096
1097    /// Check if a match expression on an enum's `.variant` property covers all variants.
1098    /// Extract narrowing info from nil-check conditions like `x != nil`.
1099    /// Returns (var_name, narrowed_type) where narrowed_type removes nil from a union.
1100    /// Check if a type satisfies an interface (Go-style implicit satisfaction).
1101    /// A type satisfies an interface if its impl block has all the required methods.
1102    fn satisfies_interface(
1103        &self,
1104        type_name: &str,
1105        interface_name: &str,
1106        scope: &TypeScope,
1107    ) -> bool {
1108        self.interface_mismatch_reason(type_name, interface_name, scope)
1109            .is_none()
1110    }
1111
1112    /// Return a detailed reason why a type does not satisfy an interface, or None
1113    /// if it does satisfy it.  Used for producing actionable warning messages.
1114    fn interface_mismatch_reason(
1115        &self,
1116        type_name: &str,
1117        interface_name: &str,
1118        scope: &TypeScope,
1119    ) -> Option<String> {
1120        let interface_methods = match scope.get_interface(interface_name) {
1121            Some(methods) => methods,
1122            None => return Some(format!("interface '{}' not found", interface_name)),
1123        };
1124        let impl_methods = match scope.get_impl_methods(type_name) {
1125            Some(methods) => methods,
1126            None => {
1127                if interface_methods.is_empty() {
1128                    return None;
1129                }
1130                let names: Vec<_> = interface_methods.iter().map(|m| m.name.as_str()).collect();
1131                return Some(format!("missing method(s): {}", names.join(", ")));
1132            }
1133        };
1134        for iface_method in interface_methods {
1135            let iface_params: Vec<_> = iface_method
1136                .params
1137                .iter()
1138                .filter(|p| p.name != "self")
1139                .collect();
1140            let iface_param_count = iface_params.len();
1141            let matching_impl = impl_methods.iter().find(|im| im.name == iface_method.name);
1142            let impl_method = match matching_impl {
1143                Some(m) => m,
1144                None => {
1145                    return Some(format!("missing method '{}'", iface_method.name));
1146                }
1147            };
1148            if impl_method.param_count != iface_param_count {
1149                return Some(format!(
1150                    "method '{}' has {} parameter(s), expected {}",
1151                    iface_method.name, impl_method.param_count, iface_param_count
1152                ));
1153            }
1154            // Check parameter types where both sides specify them
1155            for (i, iface_param) in iface_params.iter().enumerate() {
1156                if let (Some(expected), Some(actual)) = (
1157                    &iface_param.type_expr,
1158                    impl_method.param_types.get(i).and_then(|t| t.as_ref()),
1159                ) {
1160                    if !self.types_compatible(expected, actual, scope) {
1161                        return Some(format!(
1162                            "method '{}' parameter {} has type '{}', expected '{}'",
1163                            iface_method.name,
1164                            i + 1,
1165                            format_type(actual),
1166                            format_type(expected),
1167                        ));
1168                    }
1169                }
1170            }
1171            // Check return type where both sides specify it
1172            if let (Some(expected_ret), Some(actual_ret)) =
1173                (&iface_method.return_type, &impl_method.return_type)
1174            {
1175                if !self.types_compatible(expected_ret, actual_ret, scope) {
1176                    return Some(format!(
1177                        "method '{}' returns '{}', expected '{}'",
1178                        iface_method.name,
1179                        format_type(actual_ret),
1180                        format_type(expected_ret),
1181                    ));
1182                }
1183            }
1184        }
1185        None
1186    }
1187
1188    /// Recursively extract type parameter bindings from matching param/arg types.
1189    /// E.g., param_type=list<T> + arg_type=list<Dog> → binds T=Dog.
1190    fn extract_type_bindings(
1191        param_type: &TypeExpr,
1192        arg_type: &TypeExpr,
1193        type_params: &std::collections::BTreeSet<String>,
1194        bindings: &mut BTreeMap<String, String>,
1195    ) {
1196        match (param_type, arg_type) {
1197            // Direct type param match: T → concrete
1198            (TypeExpr::Named(param_name), TypeExpr::Named(concrete))
1199                if type_params.contains(param_name) =>
1200            {
1201                bindings
1202                    .entry(param_name.clone())
1203                    .or_insert(concrete.clone());
1204            }
1205            // list<T> + list<Dog>
1206            (TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
1207                Self::extract_type_bindings(p_inner, a_inner, type_params, bindings);
1208            }
1209            // dict<K, V> + dict<string, int>
1210            (TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
1211                Self::extract_type_bindings(pk, ak, type_params, bindings);
1212                Self::extract_type_bindings(pv, av, type_params, bindings);
1213            }
1214            _ => {}
1215        }
1216    }
1217
1218    fn extract_nil_narrowing(
1219        condition: &SNode,
1220        scope: &TypeScope,
1221    ) -> Option<(String, InferredType)> {
1222        if let Node::BinaryOp { op, left, right } = &condition.node {
1223            if op == "!=" {
1224                // Check for `x != nil` or `nil != x`
1225                let (var_node, nil_node) = if matches!(right.node, Node::NilLiteral) {
1226                    (left, right)
1227                } else if matches!(left.node, Node::NilLiteral) {
1228                    (right, left)
1229                } else {
1230                    return None;
1231                };
1232                let _ = nil_node;
1233                if let Node::Identifier(name) = &var_node.node {
1234                    // Look up the variable's type and narrow it
1235                    if let Some(Some(TypeExpr::Union(members))) = scope.get_var(name) {
1236                        let narrowed: Vec<TypeExpr> = members
1237                            .iter()
1238                            .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
1239                            .cloned()
1240                            .collect();
1241                        return if narrowed.len() == 1 {
1242                            Some((name.clone(), Some(narrowed.into_iter().next().unwrap())))
1243                        } else if narrowed.is_empty() {
1244                            None
1245                        } else {
1246                            Some((name.clone(), Some(TypeExpr::Union(narrowed))))
1247                        };
1248                    }
1249                }
1250            }
1251        }
1252        None
1253    }
1254
1255    fn check_match_exhaustiveness(
1256        &mut self,
1257        value: &SNode,
1258        arms: &[MatchArm],
1259        scope: &TypeScope,
1260        span: Span,
1261    ) {
1262        // Detect pattern: match <expr>.variant { "VariantA" -> ... }
1263        let enum_name = match &value.node {
1264            Node::PropertyAccess { object, property } if property == "variant" => {
1265                // Infer the type of the object
1266                match self.infer_type(object, scope) {
1267                    Some(TypeExpr::Named(name)) => {
1268                        if scope.get_enum(&name).is_some() {
1269                            Some(name)
1270                        } else {
1271                            None
1272                        }
1273                    }
1274                    _ => None,
1275                }
1276            }
1277            _ => {
1278                // Direct match on an enum value: match <expr> { ... }
1279                match self.infer_type(value, scope) {
1280                    Some(TypeExpr::Named(name)) if scope.get_enum(&name).is_some() => Some(name),
1281                    _ => None,
1282                }
1283            }
1284        };
1285
1286        let Some(enum_name) = enum_name else {
1287            return;
1288        };
1289        let Some(variants) = scope.get_enum(&enum_name) else {
1290            return;
1291        };
1292
1293        // Collect variant names covered by match arms
1294        let mut covered: Vec<String> = Vec::new();
1295        let mut has_wildcard = false;
1296
1297        for arm in arms {
1298            match &arm.pattern.node {
1299                // String literal pattern (matching on .variant): "VariantA"
1300                Node::StringLiteral(s) => covered.push(s.clone()),
1301                // Identifier pattern acts as a wildcard/catch-all
1302                Node::Identifier(name) if name == "_" || !variants.contains(name) => {
1303                    has_wildcard = true;
1304                }
1305                // Direct enum construct pattern: EnumName.Variant
1306                Node::EnumConstruct { variant, .. } => covered.push(variant.clone()),
1307                // PropertyAccess pattern: EnumName.Variant (no args)
1308                Node::PropertyAccess { property, .. } => covered.push(property.clone()),
1309                _ => {
1310                    // Unknown pattern shape — conservatively treat as wildcard
1311                    has_wildcard = true;
1312                }
1313            }
1314        }
1315
1316        if has_wildcard {
1317            return;
1318        }
1319
1320        let missing: Vec<&String> = variants.iter().filter(|v| !covered.contains(v)).collect();
1321        if !missing.is_empty() {
1322            let missing_str = missing
1323                .iter()
1324                .map(|s| format!("\"{}\"", s))
1325                .collect::<Vec<_>>()
1326                .join(", ");
1327            self.warning_at(
1328                format!(
1329                    "Non-exhaustive match on enum {}: missing variants {}",
1330                    enum_name, missing_str
1331                ),
1332                span,
1333            );
1334        }
1335    }
1336
1337    fn check_call(&mut self, name: &str, args: &[SNode], scope: &mut TypeScope, span: Span) {
1338        // Check against known function signatures
1339        let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
1340        if let Some(sig) = scope.get_fn(name).cloned() {
1341            if !has_spread
1342                && !is_builtin(name)
1343                && (args.len() < sig.required_params || args.len() > sig.params.len())
1344            {
1345                let expected = if sig.required_params == sig.params.len() {
1346                    format!("{}", sig.params.len())
1347                } else {
1348                    format!("{}-{}", sig.required_params, sig.params.len())
1349                };
1350                self.warning_at(
1351                    format!(
1352                        "Function '{}' expects {} arguments, got {}",
1353                        name,
1354                        expected,
1355                        args.len()
1356                    ),
1357                    span,
1358                );
1359            }
1360            // Build a scope that includes the function's generic type params
1361            // so they are treated as compatible with any concrete type.
1362            let call_scope = if sig.type_param_names.is_empty() {
1363                scope.clone()
1364            } else {
1365                let mut s = scope.child();
1366                for tp_name in &sig.type_param_names {
1367                    s.generic_type_params.insert(tp_name.clone());
1368                }
1369                s
1370            };
1371            for (i, (arg, (param_name, param_type))) in
1372                args.iter().zip(sig.params.iter()).enumerate()
1373            {
1374                if let Some(expected) = param_type {
1375                    let actual = self.infer_type(arg, scope);
1376                    if let Some(actual) = &actual {
1377                        if !self.types_compatible(expected, actual, &call_scope) {
1378                            self.error_at(
1379                                format!(
1380                                    "Argument {} ('{}'): expected {}, got {}",
1381                                    i + 1,
1382                                    param_name,
1383                                    format_type(expected),
1384                                    format_type(actual)
1385                                ),
1386                                arg.span,
1387                            );
1388                        }
1389                    }
1390                }
1391            }
1392            // Enforce where-clause constraints at call site
1393            if !sig.where_clauses.is_empty() {
1394                // Build mapping: type_param → concrete type from inferred args.
1395                // Recursively walks Generic types so list<T> + list<Dog> binds T=Dog.
1396                let mut type_bindings: BTreeMap<String, String> = BTreeMap::new();
1397                let type_param_set: std::collections::BTreeSet<String> =
1398                    sig.type_param_names.iter().cloned().collect();
1399                for (arg, (_param_name, param_type)) in args.iter().zip(sig.params.iter()) {
1400                    if let Some(param_ty) = param_type {
1401                        if let Some(arg_ty) = self.infer_type(arg, scope) {
1402                            Self::extract_type_bindings(
1403                                param_ty,
1404                                &arg_ty,
1405                                &type_param_set,
1406                                &mut type_bindings,
1407                            );
1408                        }
1409                    }
1410                }
1411                for (type_param, bound) in &sig.where_clauses {
1412                    if let Some(concrete_type) = type_bindings.get(type_param) {
1413                        if let Some(reason) =
1414                            self.interface_mismatch_reason(concrete_type, bound, scope)
1415                        {
1416                            self.warning_at(
1417                                format!(
1418                                    "Type '{}' does not satisfy interface '{}': {} \
1419                                     (required by constraint `where {}: {}`)",
1420                                    concrete_type, bound, reason, type_param, bound
1421                                ),
1422                                span,
1423                            );
1424                        }
1425                    }
1426                }
1427            }
1428        }
1429        // Check args recursively
1430        for arg in args {
1431            self.check_node(arg, scope);
1432        }
1433    }
1434
1435    /// Infer the type of an expression.
1436    fn infer_type(&self, snode: &SNode, scope: &TypeScope) -> InferredType {
1437        match &snode.node {
1438            Node::IntLiteral(_) => Some(TypeExpr::Named("int".into())),
1439            Node::FloatLiteral(_) => Some(TypeExpr::Named("float".into())),
1440            Node::StringLiteral(_) | Node::InterpolatedString(_) => {
1441                Some(TypeExpr::Named("string".into()))
1442            }
1443            Node::BoolLiteral(_) => Some(TypeExpr::Named("bool".into())),
1444            Node::NilLiteral => Some(TypeExpr::Named("nil".into())),
1445            Node::ListLiteral(_) => Some(TypeExpr::Named("list".into())),
1446            Node::DictLiteral(entries) => {
1447                // Infer shape type when all keys are string literals
1448                let mut fields = Vec::new();
1449                let mut all_string_keys = true;
1450                for entry in entries {
1451                    if let Node::StringLiteral(key) = &entry.key.node {
1452                        let val_type = self
1453                            .infer_type(&entry.value, scope)
1454                            .unwrap_or(TypeExpr::Named("nil".into()));
1455                        fields.push(ShapeField {
1456                            name: key.clone(),
1457                            type_expr: val_type,
1458                            optional: false,
1459                        });
1460                    } else {
1461                        all_string_keys = false;
1462                        break;
1463                    }
1464                }
1465                if all_string_keys && !fields.is_empty() {
1466                    Some(TypeExpr::Shape(fields))
1467                } else {
1468                    Some(TypeExpr::Named("dict".into()))
1469                }
1470            }
1471            Node::Closure { params, body, .. } => {
1472                // If all params are typed and we can infer a return type, produce FnType
1473                let all_typed = params.iter().all(|p| p.type_expr.is_some());
1474                if all_typed && !params.is_empty() {
1475                    let param_types: Vec<TypeExpr> =
1476                        params.iter().filter_map(|p| p.type_expr.clone()).collect();
1477                    // Try to infer return type from last expression in body
1478                    let ret = body.last().and_then(|last| self.infer_type(last, scope));
1479                    if let Some(ret_type) = ret {
1480                        return Some(TypeExpr::FnType {
1481                            params: param_types,
1482                            return_type: Box::new(ret_type),
1483                        });
1484                    }
1485                }
1486                Some(TypeExpr::Named("closure".into()))
1487            }
1488
1489            Node::Identifier(name) => scope.get_var(name).cloned().flatten(),
1490
1491            Node::FunctionCall { name, .. } => {
1492                // Struct constructor calls return the struct type
1493                if scope.get_struct(name).is_some() {
1494                    return Some(TypeExpr::Named(name.clone()));
1495                }
1496                // Check user-defined function return types
1497                if let Some(sig) = scope.get_fn(name) {
1498                    return sig.return_type.clone();
1499                }
1500                // Check builtin return types
1501                builtin_return_type(name)
1502            }
1503
1504            Node::BinaryOp { op, left, right } => {
1505                let lt = self.infer_type(left, scope);
1506                let rt = self.infer_type(right, scope);
1507                infer_binary_op_type(op, &lt, &rt)
1508            }
1509
1510            Node::UnaryOp { op, operand } => {
1511                let t = self.infer_type(operand, scope);
1512                match op.as_str() {
1513                    "!" => Some(TypeExpr::Named("bool".into())),
1514                    "-" => t, // negation preserves type
1515                    _ => None,
1516                }
1517            }
1518
1519            Node::Ternary {
1520                true_expr,
1521                false_expr,
1522                ..
1523            } => {
1524                let tt = self.infer_type(true_expr, scope);
1525                let ft = self.infer_type(false_expr, scope);
1526                match (&tt, &ft) {
1527                    (Some(a), Some(b)) if a == b => tt,
1528                    (Some(a), Some(b)) => Some(TypeExpr::Union(vec![a.clone(), b.clone()])),
1529                    (Some(_), None) => tt,
1530                    (None, Some(_)) => ft,
1531                    (None, None) => None,
1532                }
1533            }
1534
1535            Node::EnumConstruct { enum_name, .. } => Some(TypeExpr::Named(enum_name.clone())),
1536
1537            Node::PropertyAccess { object, property } => {
1538                // EnumName.Variant → infer as the enum type
1539                if let Node::Identifier(name) = &object.node {
1540                    if scope.get_enum(name).is_some() {
1541                        return Some(TypeExpr::Named(name.clone()));
1542                    }
1543                }
1544                // .variant on an enum value → string
1545                if property == "variant" {
1546                    let obj_type = self.infer_type(object, scope);
1547                    if let Some(TypeExpr::Named(name)) = &obj_type {
1548                        if scope.get_enum(name).is_some() {
1549                            return Some(TypeExpr::Named("string".into()));
1550                        }
1551                    }
1552                }
1553                // Shape field access: obj.field → field type
1554                let obj_type = self.infer_type(object, scope);
1555                if let Some(TypeExpr::Shape(fields)) = &obj_type {
1556                    if let Some(field) = fields.iter().find(|f| f.name == *property) {
1557                        return Some(field.type_expr.clone());
1558                    }
1559                }
1560                None
1561            }
1562
1563            Node::SubscriptAccess { object, index } => {
1564                let obj_type = self.infer_type(object, scope);
1565                match &obj_type {
1566                    Some(TypeExpr::List(inner)) => Some(*inner.clone()),
1567                    Some(TypeExpr::DictType(_, v)) => Some(*v.clone()),
1568                    Some(TypeExpr::Shape(fields)) => {
1569                        // If index is a string literal, look up the field type
1570                        if let Node::StringLiteral(key) = &index.node {
1571                            fields
1572                                .iter()
1573                                .find(|f| &f.name == key)
1574                                .map(|f| f.type_expr.clone())
1575                        } else {
1576                            None
1577                        }
1578                    }
1579                    Some(TypeExpr::Named(n)) if n == "list" => None,
1580                    Some(TypeExpr::Named(n)) if n == "dict" => None,
1581                    Some(TypeExpr::Named(n)) if n == "string" => {
1582                        Some(TypeExpr::Named("string".into()))
1583                    }
1584                    _ => None,
1585                }
1586            }
1587            Node::SliceAccess { object, .. } => {
1588                // Slicing a list returns the same list type; slicing a string returns string
1589                let obj_type = self.infer_type(object, scope);
1590                match &obj_type {
1591                    Some(TypeExpr::List(_)) => obj_type,
1592                    Some(TypeExpr::Named(n)) if n == "list" => obj_type,
1593                    Some(TypeExpr::Named(n)) if n == "string" => {
1594                        Some(TypeExpr::Named("string".into()))
1595                    }
1596                    _ => None,
1597                }
1598            }
1599            Node::MethodCall { object, method, .. }
1600            | Node::OptionalMethodCall { object, method, .. } => {
1601                let obj_type = self.infer_type(object, scope);
1602                let is_dict = matches!(&obj_type, Some(TypeExpr::Named(n)) if n == "dict")
1603                    || matches!(&obj_type, Some(TypeExpr::DictType(..)))
1604                    || matches!(&obj_type, Some(TypeExpr::Shape(_)));
1605                match method.as_str() {
1606                    // Shared: bool-returning methods
1607                    "contains" | "starts_with" | "ends_with" | "empty" | "has" | "any" | "all" => {
1608                        Some(TypeExpr::Named("bool".into()))
1609                    }
1610                    // Shared: int-returning methods
1611                    "count" | "index_of" => Some(TypeExpr::Named("int".into())),
1612                    // String methods
1613                    "trim" | "lowercase" | "uppercase" | "reverse" | "replace" | "substring"
1614                    | "pad_left" | "pad_right" | "repeat" | "join" => {
1615                        Some(TypeExpr::Named("string".into()))
1616                    }
1617                    "split" | "chars" => Some(TypeExpr::Named("list".into())),
1618                    // filter returns dict for dicts, list for lists
1619                    "filter" => {
1620                        if is_dict {
1621                            Some(TypeExpr::Named("dict".into()))
1622                        } else {
1623                            Some(TypeExpr::Named("list".into()))
1624                        }
1625                    }
1626                    // List methods
1627                    "map" | "flat_map" | "sort" => Some(TypeExpr::Named("list".into())),
1628                    "reduce" | "find" | "first" | "last" => None,
1629                    // Dict methods
1630                    "keys" | "values" | "entries" => Some(TypeExpr::Named("list".into())),
1631                    "merge" | "map_values" | "rekey" | "map_keys" => {
1632                        // Rekey/map_keys transform keys; resulting dict still keys-by-string.
1633                        // Preserve the value-type parameter when known so downstream code can
1634                        // still rely on dict<string, V> typing after a key-rename.
1635                        if let Some(TypeExpr::DictType(_, v)) = &obj_type {
1636                            Some(TypeExpr::DictType(
1637                                Box::new(TypeExpr::Named("string".into())),
1638                                v.clone(),
1639                            ))
1640                        } else {
1641                            Some(TypeExpr::Named("dict".into()))
1642                        }
1643                    }
1644                    // Conversions
1645                    "to_string" => Some(TypeExpr::Named("string".into())),
1646                    "to_int" => Some(TypeExpr::Named("int".into())),
1647                    "to_float" => Some(TypeExpr::Named("float".into())),
1648                    _ => None,
1649                }
1650            }
1651
1652            // TryOperator on Result<T, E> produces T
1653            Node::TryOperator { operand } => {
1654                match self.infer_type(operand, scope) {
1655                    Some(TypeExpr::Named(name)) if name == "Result" => None, // unknown inner type
1656                    _ => None,
1657                }
1658            }
1659
1660            _ => None,
1661        }
1662    }
1663
1664    /// Check if two types are compatible (actual can be assigned to expected).
1665    fn types_compatible(&self, expected: &TypeExpr, actual: &TypeExpr, scope: &TypeScope) -> bool {
1666        // Generic type parameters match anything.
1667        if let TypeExpr::Named(name) = expected {
1668            if scope.is_generic_type_param(name) {
1669                return true;
1670            }
1671        }
1672        if let TypeExpr::Named(name) = actual {
1673            if scope.is_generic_type_param(name) {
1674                return true;
1675            }
1676        }
1677        let expected = self.resolve_alias(expected, scope);
1678        let actual = self.resolve_alias(actual, scope);
1679
1680        // Interface satisfaction: if expected is an interface name, check if actual type
1681        // has all required methods (Go-style implicit satisfaction).
1682        if let TypeExpr::Named(iface_name) = &expected {
1683            if scope.get_interface(iface_name).is_some() {
1684                if let TypeExpr::Named(type_name) = &actual {
1685                    return self.satisfies_interface(type_name, iface_name, scope);
1686                }
1687                return false;
1688            }
1689        }
1690
1691        match (&expected, &actual) {
1692            (TypeExpr::Named(a), TypeExpr::Named(b)) => a == b || (a == "float" && b == "int"),
1693            (TypeExpr::Union(members), actual_type) => members
1694                .iter()
1695                .any(|m| self.types_compatible(m, actual_type, scope)),
1696            (expected_type, TypeExpr::Union(members)) => members
1697                .iter()
1698                .all(|m| self.types_compatible(expected_type, m, scope)),
1699            (TypeExpr::Shape(_), TypeExpr::Named(n)) if n == "dict" => true,
1700            (TypeExpr::Named(n), TypeExpr::Shape(_)) if n == "dict" => true,
1701            (TypeExpr::Shape(ef), TypeExpr::Shape(af)) => ef.iter().all(|expected_field| {
1702                if expected_field.optional {
1703                    return true;
1704                }
1705                af.iter().any(|actual_field| {
1706                    actual_field.name == expected_field.name
1707                        && self.types_compatible(
1708                            &expected_field.type_expr,
1709                            &actual_field.type_expr,
1710                            scope,
1711                        )
1712                })
1713            }),
1714            // dict<K, V> expected, Shape actual → all field values must match V
1715            (TypeExpr::DictType(ek, ev), TypeExpr::Shape(af)) => {
1716                let keys_ok = matches!(ek.as_ref(), TypeExpr::Named(n) if n == "string");
1717                keys_ok
1718                    && af
1719                        .iter()
1720                        .all(|f| self.types_compatible(ev, &f.type_expr, scope))
1721            }
1722            // Shape expected, dict<K, V> actual → gradual: allow since dict may have the fields
1723            (TypeExpr::Shape(_), TypeExpr::DictType(_, _)) => true,
1724            (TypeExpr::List(expected_inner), TypeExpr::List(actual_inner)) => {
1725                self.types_compatible(expected_inner, actual_inner, scope)
1726            }
1727            (TypeExpr::Named(n), TypeExpr::List(_)) if n == "list" => true,
1728            (TypeExpr::List(_), TypeExpr::Named(n)) if n == "list" => true,
1729            (TypeExpr::DictType(ek, ev), TypeExpr::DictType(ak, av)) => {
1730                self.types_compatible(ek, ak, scope) && self.types_compatible(ev, av, scope)
1731            }
1732            (TypeExpr::Named(n), TypeExpr::DictType(_, _)) if n == "dict" => true,
1733            (TypeExpr::DictType(_, _), TypeExpr::Named(n)) if n == "dict" => true,
1734            // FnType compatibility: params match positionally and return types match
1735            (
1736                TypeExpr::FnType {
1737                    params: ep,
1738                    return_type: er,
1739                },
1740                TypeExpr::FnType {
1741                    params: ap,
1742                    return_type: ar,
1743                },
1744            ) => {
1745                ep.len() == ap.len()
1746                    && ep
1747                        .iter()
1748                        .zip(ap.iter())
1749                        .all(|(e, a)| self.types_compatible(e, a, scope))
1750                    && self.types_compatible(er, ar, scope)
1751            }
1752            // FnType is compatible with Named("closure") for backward compat
1753            (TypeExpr::FnType { .. }, TypeExpr::Named(n)) if n == "closure" => true,
1754            (TypeExpr::Named(n), TypeExpr::FnType { .. }) if n == "closure" => true,
1755            _ => false,
1756        }
1757    }
1758
1759    fn resolve_alias<'a>(&self, ty: &'a TypeExpr, scope: &'a TypeScope) -> TypeExpr {
1760        if let TypeExpr::Named(name) = ty {
1761            if let Some(resolved) = scope.resolve_type(name) {
1762                return resolved.clone();
1763            }
1764        }
1765        ty.clone()
1766    }
1767
1768    fn error_at(&mut self, message: String, span: Span) {
1769        self.diagnostics.push(TypeDiagnostic {
1770            message,
1771            severity: DiagnosticSeverity::Error,
1772            span: Some(span),
1773            help: None,
1774        });
1775    }
1776
1777    #[allow(dead_code)]
1778    fn error_at_with_help(&mut self, message: String, span: Span, help: String) {
1779        self.diagnostics.push(TypeDiagnostic {
1780            message,
1781            severity: DiagnosticSeverity::Error,
1782            span: Some(span),
1783            help: Some(help),
1784        });
1785    }
1786
1787    fn warning_at(&mut self, message: String, span: Span) {
1788        self.diagnostics.push(TypeDiagnostic {
1789            message,
1790            severity: DiagnosticSeverity::Warning,
1791            span: Some(span),
1792            help: None,
1793        });
1794    }
1795
1796    #[allow(dead_code)]
1797    fn warning_at_with_help(&mut self, message: String, span: Span, help: String) {
1798        self.diagnostics.push(TypeDiagnostic {
1799            message,
1800            severity: DiagnosticSeverity::Warning,
1801            span: Some(span),
1802            help: Some(help),
1803        });
1804    }
1805}
1806
1807impl Default for TypeChecker {
1808    fn default() -> Self {
1809        Self::new()
1810    }
1811}
1812
1813/// Infer the result type of a binary operation.
1814fn infer_binary_op_type(op: &str, left: &InferredType, right: &InferredType) -> InferredType {
1815    match op {
1816        "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||" | "in" | "not_in" => {
1817            Some(TypeExpr::Named("bool".into()))
1818        }
1819        "+" => match (left, right) {
1820            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
1821                match (l.as_str(), r.as_str()) {
1822                    ("int", "int") => Some(TypeExpr::Named("int".into())),
1823                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
1824                    ("string", _) => Some(TypeExpr::Named("string".into())),
1825                    ("list", "list") => Some(TypeExpr::Named("list".into())),
1826                    ("dict", "dict") => Some(TypeExpr::Named("dict".into())),
1827                    _ => Some(TypeExpr::Named("string".into())),
1828                }
1829            }
1830            _ => None,
1831        },
1832        "-" | "*" | "/" | "%" => match (left, right) {
1833            (Some(TypeExpr::Named(l)), Some(TypeExpr::Named(r))) => {
1834                match (l.as_str(), r.as_str()) {
1835                    ("int", "int") => Some(TypeExpr::Named("int".into())),
1836                    ("float", _) | (_, "float") => Some(TypeExpr::Named("float".into())),
1837                    _ => None,
1838                }
1839            }
1840            _ => None,
1841        },
1842        "??" => match (left, right) {
1843            (Some(TypeExpr::Union(members)), _) => {
1844                let non_nil: Vec<_> = members
1845                    .iter()
1846                    .filter(|m| !matches!(m, TypeExpr::Named(n) if n == "nil"))
1847                    .cloned()
1848                    .collect();
1849                if non_nil.len() == 1 {
1850                    Some(non_nil[0].clone())
1851                } else if non_nil.is_empty() {
1852                    right.clone()
1853                } else {
1854                    Some(TypeExpr::Union(non_nil))
1855                }
1856            }
1857            _ => right.clone(),
1858        },
1859        "|>" => None,
1860        _ => None,
1861    }
1862}
1863
1864/// Format a type expression for display in error messages.
1865/// Produce a detail string describing why a Shape type is incompatible with
1866/// another Shape type — e.g. "missing field 'age' (int)" or "field 'name'
1867/// has type int, expected string".  Returns `None` if both types are not shapes.
1868pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
1869    if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
1870        let mut details = Vec::new();
1871        for field in ef {
1872            if field.optional {
1873                continue;
1874            }
1875            match af.iter().find(|f| f.name == field.name) {
1876                None => details.push(format!(
1877                    "missing field '{}' ({})",
1878                    field.name,
1879                    format_type(&field.type_expr)
1880                )),
1881                Some(actual_field) => {
1882                    let e_str = format_type(&field.type_expr);
1883                    let a_str = format_type(&actual_field.type_expr);
1884                    if e_str != a_str {
1885                        details.push(format!(
1886                            "field '{}' has type {}, expected {}",
1887                            field.name, a_str, e_str
1888                        ));
1889                    }
1890                }
1891            }
1892        }
1893        if details.is_empty() {
1894            None
1895        } else {
1896            Some(details.join("; "))
1897        }
1898    } else {
1899        None
1900    }
1901}
1902
1903pub fn format_type(ty: &TypeExpr) -> String {
1904    match ty {
1905        TypeExpr::Named(n) => n.clone(),
1906        TypeExpr::Union(types) => types
1907            .iter()
1908            .map(format_type)
1909            .collect::<Vec<_>>()
1910            .join(" | "),
1911        TypeExpr::Shape(fields) => {
1912            let inner: Vec<String> = fields
1913                .iter()
1914                .map(|f| {
1915                    let opt = if f.optional { "?" } else { "" };
1916                    format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
1917                })
1918                .collect();
1919            format!("{{{}}}", inner.join(", "))
1920        }
1921        TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
1922        TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
1923        TypeExpr::FnType {
1924            params,
1925            return_type,
1926        } => {
1927            let params_str = params
1928                .iter()
1929                .map(format_type)
1930                .collect::<Vec<_>>()
1931                .join(", ");
1932            format!("fn({}) -> {}", params_str, format_type(return_type))
1933        }
1934    }
1935}
1936
1937#[cfg(test)]
1938mod tests {
1939    use super::*;
1940    use crate::Parser;
1941    use harn_lexer::Lexer;
1942
1943    fn check_source(source: &str) -> Vec<TypeDiagnostic> {
1944        let mut lexer = Lexer::new(source);
1945        let tokens = lexer.tokenize().unwrap();
1946        let mut parser = Parser::new(tokens);
1947        let program = parser.parse().unwrap();
1948        TypeChecker::new().check(&program)
1949    }
1950
1951    fn errors(source: &str) -> Vec<String> {
1952        check_source(source)
1953            .into_iter()
1954            .filter(|d| d.severity == DiagnosticSeverity::Error)
1955            .map(|d| d.message)
1956            .collect()
1957    }
1958
1959    #[test]
1960    fn test_no_errors_for_untyped_code() {
1961        let errs = errors("pipeline t(task) { let x = 42\nlog(x) }");
1962        assert!(errs.is_empty());
1963    }
1964
1965    #[test]
1966    fn test_correct_typed_let() {
1967        let errs = errors("pipeline t(task) { let x: int = 42 }");
1968        assert!(errs.is_empty());
1969    }
1970
1971    #[test]
1972    fn test_type_mismatch_let() {
1973        let errs = errors(r#"pipeline t(task) { let x: int = "hello" }"#);
1974        assert_eq!(errs.len(), 1);
1975        assert!(errs[0].contains("Type mismatch"));
1976        assert!(errs[0].contains("int"));
1977        assert!(errs[0].contains("string"));
1978    }
1979
1980    #[test]
1981    fn test_correct_typed_fn() {
1982        let errs = errors(
1983            "pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }\nadd(1, 2) }",
1984        );
1985        assert!(errs.is_empty());
1986    }
1987
1988    #[test]
1989    fn test_fn_arg_type_mismatch() {
1990        let errs = errors(
1991            r#"pipeline t(task) { fn add(a: int, b: int) -> int { return a + b }
1992add("hello", 2) }"#,
1993        );
1994        assert_eq!(errs.len(), 1);
1995        assert!(errs[0].contains("Argument 1"));
1996        assert!(errs[0].contains("expected int"));
1997    }
1998
1999    #[test]
2000    fn test_return_type_mismatch() {
2001        let errs = errors(r#"pipeline t(task) { fn get() -> int { return "hello" } }"#);
2002        assert_eq!(errs.len(), 1);
2003        assert!(errs[0].contains("Return type mismatch"));
2004    }
2005
2006    #[test]
2007    fn test_union_type_compatible() {
2008        let errs = errors(r#"pipeline t(task) { let x: string | nil = nil }"#);
2009        assert!(errs.is_empty());
2010    }
2011
2012    #[test]
2013    fn test_union_type_mismatch() {
2014        let errs = errors(r#"pipeline t(task) { let x: string | nil = 42 }"#);
2015        assert_eq!(errs.len(), 1);
2016        assert!(errs[0].contains("Type mismatch"));
2017    }
2018
2019    #[test]
2020    fn test_type_inference_propagation() {
2021        let errs = errors(
2022            r#"pipeline t(task) {
2023  fn add(a: int, b: int) -> int { return a + b }
2024  let result: string = add(1, 2)
2025}"#,
2026        );
2027        assert_eq!(errs.len(), 1);
2028        assert!(errs[0].contains("Type mismatch"));
2029        assert!(errs[0].contains("string"));
2030        assert!(errs[0].contains("int"));
2031    }
2032
2033    #[test]
2034    fn test_builtin_return_type_inference() {
2035        let errs = errors(r#"pipeline t(task) { let x: string = to_int("42") }"#);
2036        assert_eq!(errs.len(), 1);
2037        assert!(errs[0].contains("string"));
2038        assert!(errs[0].contains("int"));
2039    }
2040
2041    #[test]
2042    fn test_workflow_and_transcript_builtins_are_known() {
2043        let errs = errors(
2044            r#"pipeline t(task) {
2045  let flow = workflow_graph({name: "demo", entry: "act", nodes: {act: {kind: "stage"}}})
2046  let report: dict = workflow_policy_report(flow, {tools: tool_registry(), capabilities: {workspace: ["read_text"]}})
2047  let run: dict = workflow_execute("task", flow, [], {})
2048  let tree: dict = load_run_tree("run.json")
2049  let fixture: dict = run_record_fixture(run?.run)
2050  let suite: dict = run_record_eval_suite([{run: run?.run, fixture: fixture}])
2051  let diff: dict = run_record_diff(run?.run, run?.run)
2052  let manifest: dict = eval_suite_manifest({cases: [{run_path: "run.json"}]})
2053  let suite_report: dict = eval_suite_run(manifest)
2054  let wf: dict = artifact_workspace_file("src/main.rs", "fn main() {}", {source: "host"})
2055  let snap: dict = artifact_workspace_snapshot(["src/main.rs"], "snapshot")
2056  let selection: dict = artifact_editor_selection("src/main.rs", "main")
2057  let verify: dict = artifact_verification_result("verify", "ok")
2058  let test_result: dict = artifact_test_result("tests", "pass")
2059  let cmd: dict = artifact_command_result("cargo test", {status: 0})
2060  let patch: dict = artifact_diff("src/main.rs", "old", "new")
2061  let git: dict = artifact_git_diff("diff --git a b")
2062  let review: dict = artifact_diff_review(patch, "review me")
2063  let decision: dict = artifact_review_decision(review, "accepted")
2064  let proposal: dict = artifact_patch_proposal(review, "*** Begin Patch")
2065  let bundle: dict = artifact_verification_bundle("checks", [{name: "fmt", ok: true}])
2066  let apply: dict = artifact_apply_intent(review, "apply")
2067  let transcript = transcript_reset({metadata: {source: "test"}})
2068  let visible: string = transcript_render_visible(transcript_archive(transcript))
2069  let events: list = transcript_events(transcript)
2070  let context: string = artifact_context([], {max_artifacts: 1})
2071  println(report)
2072  println(run)
2073  println(tree)
2074  println(fixture)
2075  println(suite)
2076  println(diff)
2077  println(manifest)
2078  println(suite_report)
2079  println(wf)
2080  println(snap)
2081  println(selection)
2082  println(verify)
2083  println(test_result)
2084  println(cmd)
2085  println(patch)
2086  println(git)
2087  println(review)
2088  println(decision)
2089  println(proposal)
2090  println(bundle)
2091  println(apply)
2092  println(visible)
2093  println(events)
2094  println(context)
2095}"#,
2096        );
2097        assert!(errs.is_empty(), "unexpected type errors: {errs:?}");
2098    }
2099
2100    #[test]
2101    fn test_binary_op_type_inference() {
2102        let errs = errors("pipeline t(task) { let x: string = 1 + 2 }");
2103        assert_eq!(errs.len(), 1);
2104    }
2105
2106    #[test]
2107    fn test_comparison_returns_bool() {
2108        let errs = errors("pipeline t(task) { let x: bool = 1 < 2 }");
2109        assert!(errs.is_empty());
2110    }
2111
2112    #[test]
2113    fn test_int_float_promotion() {
2114        let errs = errors("pipeline t(task) { let x: float = 42 }");
2115        assert!(errs.is_empty());
2116    }
2117
2118    #[test]
2119    fn test_untyped_code_no_errors() {
2120        let errs = errors(
2121            r#"pipeline t(task) {
2122  fn process(data) {
2123    let result = data + " processed"
2124    return result
2125  }
2126  log(process("hello"))
2127}"#,
2128        );
2129        assert!(errs.is_empty());
2130    }
2131
2132    #[test]
2133    fn test_type_alias() {
2134        let errs = errors(
2135            r#"pipeline t(task) {
2136  type Name = string
2137  let x: Name = "hello"
2138}"#,
2139        );
2140        assert!(errs.is_empty());
2141    }
2142
2143    #[test]
2144    fn test_type_alias_mismatch() {
2145        let errs = errors(
2146            r#"pipeline t(task) {
2147  type Name = string
2148  let x: Name = 42
2149}"#,
2150        );
2151        assert_eq!(errs.len(), 1);
2152    }
2153
2154    #[test]
2155    fn test_assignment_type_check() {
2156        let errs = errors(
2157            r#"pipeline t(task) {
2158  var x: int = 0
2159  x = "hello"
2160}"#,
2161        );
2162        assert_eq!(errs.len(), 1);
2163        assert!(errs[0].contains("cannot assign string"));
2164    }
2165
2166    #[test]
2167    fn test_covariance_int_to_float_in_fn() {
2168        let errs = errors(
2169            "pipeline t(task) { fn scale(x: float) -> float { return x * 2.0 }\nscale(42) }",
2170        );
2171        assert!(errs.is_empty());
2172    }
2173
2174    #[test]
2175    fn test_covariance_return_type() {
2176        let errs = errors("pipeline t(task) { fn get() -> float { return 42 } }");
2177        assert!(errs.is_empty());
2178    }
2179
2180    #[test]
2181    fn test_no_contravariance_float_to_int() {
2182        let errs = errors("pipeline t(task) { fn add(a: int) -> int { return a + 1 }\nadd(3.14) }");
2183        assert_eq!(errs.len(), 1);
2184    }
2185
2186    // --- Exhaustiveness checking tests ---
2187
2188    fn warnings(source: &str) -> Vec<String> {
2189        check_source(source)
2190            .into_iter()
2191            .filter(|d| d.severity == DiagnosticSeverity::Warning)
2192            .map(|d| d.message)
2193            .collect()
2194    }
2195
2196    #[test]
2197    fn test_exhaustive_match_no_warning() {
2198        let warns = warnings(
2199            r#"pipeline t(task) {
2200  enum Color { Red, Green, Blue }
2201  let c = Color.Red
2202  match c.variant {
2203    "Red" -> { log("r") }
2204    "Green" -> { log("g") }
2205    "Blue" -> { log("b") }
2206  }
2207}"#,
2208        );
2209        let exhaustive_warns: Vec<_> = warns
2210            .iter()
2211            .filter(|w| w.contains("Non-exhaustive"))
2212            .collect();
2213        assert!(exhaustive_warns.is_empty());
2214    }
2215
2216    #[test]
2217    fn test_non_exhaustive_match_warning() {
2218        let warns = warnings(
2219            r#"pipeline t(task) {
2220  enum Color { Red, Green, Blue }
2221  let c = Color.Red
2222  match c.variant {
2223    "Red" -> { log("r") }
2224    "Green" -> { log("g") }
2225  }
2226}"#,
2227        );
2228        let exhaustive_warns: Vec<_> = warns
2229            .iter()
2230            .filter(|w| w.contains("Non-exhaustive"))
2231            .collect();
2232        assert_eq!(exhaustive_warns.len(), 1);
2233        assert!(exhaustive_warns[0].contains("Blue"));
2234    }
2235
2236    #[test]
2237    fn test_non_exhaustive_multiple_missing() {
2238        let warns = warnings(
2239            r#"pipeline t(task) {
2240  enum Status { Active, Inactive, Pending }
2241  let s = Status.Active
2242  match s.variant {
2243    "Active" -> { log("a") }
2244  }
2245}"#,
2246        );
2247        let exhaustive_warns: Vec<_> = warns
2248            .iter()
2249            .filter(|w| w.contains("Non-exhaustive"))
2250            .collect();
2251        assert_eq!(exhaustive_warns.len(), 1);
2252        assert!(exhaustive_warns[0].contains("Inactive"));
2253        assert!(exhaustive_warns[0].contains("Pending"));
2254    }
2255
2256    #[test]
2257    fn test_enum_construct_type_inference() {
2258        let errs = errors(
2259            r#"pipeline t(task) {
2260  enum Color { Red, Green, Blue }
2261  let c: Color = Color.Red
2262}"#,
2263        );
2264        assert!(errs.is_empty());
2265    }
2266
2267    // --- Type narrowing tests ---
2268
2269    #[test]
2270    fn test_nil_coalescing_strips_nil() {
2271        // After ??, nil should be stripped from the type
2272        let errs = errors(
2273            r#"pipeline t(task) {
2274  let x: string | nil = nil
2275  let y: string = x ?? "default"
2276}"#,
2277        );
2278        assert!(errs.is_empty());
2279    }
2280
2281    #[test]
2282    fn test_shape_mismatch_detail_missing_field() {
2283        let errs = errors(
2284            r#"pipeline t(task) {
2285  let x: {name: string, age: int} = {name: "hello"}
2286}"#,
2287        );
2288        assert_eq!(errs.len(), 1);
2289        assert!(
2290            errs[0].contains("missing field 'age'"),
2291            "expected detail about missing field, got: {}",
2292            errs[0]
2293        );
2294    }
2295
2296    #[test]
2297    fn test_shape_mismatch_detail_wrong_type() {
2298        let errs = errors(
2299            r#"pipeline t(task) {
2300  let x: {name: string, age: int} = {name: 42, age: 10}
2301}"#,
2302        );
2303        assert_eq!(errs.len(), 1);
2304        assert!(
2305            errs[0].contains("field 'name' has type int, expected string"),
2306            "expected detail about wrong type, got: {}",
2307            errs[0]
2308        );
2309    }
2310
2311    // --- Match pattern type validation tests ---
2312
2313    #[test]
2314    fn test_match_pattern_string_against_int() {
2315        let warns = warnings(
2316            r#"pipeline t(task) {
2317  let x: int = 42
2318  match x {
2319    "hello" -> { log("bad") }
2320    42 -> { log("ok") }
2321  }
2322}"#,
2323        );
2324        let pattern_warns: Vec<_> = warns
2325            .iter()
2326            .filter(|w| w.contains("Match pattern type mismatch"))
2327            .collect();
2328        assert_eq!(pattern_warns.len(), 1);
2329        assert!(pattern_warns[0].contains("matching int against string literal"));
2330    }
2331
2332    #[test]
2333    fn test_match_pattern_int_against_string() {
2334        let warns = warnings(
2335            r#"pipeline t(task) {
2336  let x: string = "hello"
2337  match x {
2338    42 -> { log("bad") }
2339    "hello" -> { log("ok") }
2340  }
2341}"#,
2342        );
2343        let pattern_warns: Vec<_> = warns
2344            .iter()
2345            .filter(|w| w.contains("Match pattern type mismatch"))
2346            .collect();
2347        assert_eq!(pattern_warns.len(), 1);
2348        assert!(pattern_warns[0].contains("matching string against int literal"));
2349    }
2350
2351    #[test]
2352    fn test_match_pattern_bool_against_int() {
2353        let warns = warnings(
2354            r#"pipeline t(task) {
2355  let x: int = 42
2356  match x {
2357    true -> { log("bad") }
2358    42 -> { log("ok") }
2359  }
2360}"#,
2361        );
2362        let pattern_warns: Vec<_> = warns
2363            .iter()
2364            .filter(|w| w.contains("Match pattern type mismatch"))
2365            .collect();
2366        assert_eq!(pattern_warns.len(), 1);
2367        assert!(pattern_warns[0].contains("matching int against bool literal"));
2368    }
2369
2370    #[test]
2371    fn test_match_pattern_float_against_string() {
2372        let warns = warnings(
2373            r#"pipeline t(task) {
2374  let x: string = "hello"
2375  match x {
2376    3.14 -> { log("bad") }
2377    "hello" -> { log("ok") }
2378  }
2379}"#,
2380        );
2381        let pattern_warns: Vec<_> = warns
2382            .iter()
2383            .filter(|w| w.contains("Match pattern type mismatch"))
2384            .collect();
2385        assert_eq!(pattern_warns.len(), 1);
2386        assert!(pattern_warns[0].contains("matching string against float literal"));
2387    }
2388
2389    #[test]
2390    fn test_match_pattern_int_against_float_ok() {
2391        // int and float are compatible for match patterns
2392        let warns = warnings(
2393            r#"pipeline t(task) {
2394  let x: float = 3.14
2395  match x {
2396    42 -> { log("ok") }
2397    _ -> { log("default") }
2398  }
2399}"#,
2400        );
2401        let pattern_warns: Vec<_> = warns
2402            .iter()
2403            .filter(|w| w.contains("Match pattern type mismatch"))
2404            .collect();
2405        assert!(pattern_warns.is_empty());
2406    }
2407
2408    #[test]
2409    fn test_match_pattern_float_against_int_ok() {
2410        // float and int are compatible for match patterns
2411        let warns = warnings(
2412            r#"pipeline t(task) {
2413  let x: int = 42
2414  match x {
2415    3.14 -> { log("close") }
2416    _ -> { log("default") }
2417  }
2418}"#,
2419        );
2420        let pattern_warns: Vec<_> = warns
2421            .iter()
2422            .filter(|w| w.contains("Match pattern type mismatch"))
2423            .collect();
2424        assert!(pattern_warns.is_empty());
2425    }
2426
2427    #[test]
2428    fn test_match_pattern_correct_types_no_warning() {
2429        let warns = warnings(
2430            r#"pipeline t(task) {
2431  let x: int = 42
2432  match x {
2433    1 -> { log("one") }
2434    2 -> { log("two") }
2435    _ -> { log("other") }
2436  }
2437}"#,
2438        );
2439        let pattern_warns: Vec<_> = warns
2440            .iter()
2441            .filter(|w| w.contains("Match pattern type mismatch"))
2442            .collect();
2443        assert!(pattern_warns.is_empty());
2444    }
2445
2446    #[test]
2447    fn test_match_pattern_wildcard_no_warning() {
2448        let warns = warnings(
2449            r#"pipeline t(task) {
2450  let x: int = 42
2451  match x {
2452    _ -> { log("catch all") }
2453  }
2454}"#,
2455        );
2456        let pattern_warns: Vec<_> = warns
2457            .iter()
2458            .filter(|w| w.contains("Match pattern type mismatch"))
2459            .collect();
2460        assert!(pattern_warns.is_empty());
2461    }
2462
2463    #[test]
2464    fn test_match_pattern_untyped_no_warning() {
2465        // When value has no known type, no warning should be emitted
2466        let warns = warnings(
2467            r#"pipeline t(task) {
2468  let x = some_unknown_fn()
2469  match x {
2470    "hello" -> { log("string") }
2471    42 -> { log("int") }
2472  }
2473}"#,
2474        );
2475        let pattern_warns: Vec<_> = warns
2476            .iter()
2477            .filter(|w| w.contains("Match pattern type mismatch"))
2478            .collect();
2479        assert!(pattern_warns.is_empty());
2480    }
2481
2482    // --- Interface constraint type checking tests ---
2483
2484    fn iface_warns(source: &str) -> Vec<String> {
2485        warnings(source)
2486            .into_iter()
2487            .filter(|w| w.contains("does not satisfy interface"))
2488            .collect()
2489    }
2490
2491    #[test]
2492    fn test_interface_constraint_return_type_mismatch() {
2493        let warns = iface_warns(
2494            r#"pipeline t(task) {
2495  interface Sizable {
2496    fn size(self) -> int
2497  }
2498  struct Box { width: int }
2499  impl Box {
2500    fn size(self) -> string { return "nope" }
2501  }
2502  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2503  measure(Box({width: 3}))
2504}"#,
2505        );
2506        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2507        assert!(
2508            warns[0].contains("method 'size' returns 'string', expected 'int'"),
2509            "unexpected message: {}",
2510            warns[0]
2511        );
2512    }
2513
2514    #[test]
2515    fn test_interface_constraint_param_type_mismatch() {
2516        let warns = iface_warns(
2517            r#"pipeline t(task) {
2518  interface Processor {
2519    fn process(self, x: int) -> string
2520  }
2521  struct MyProc { name: string }
2522  impl MyProc {
2523    fn process(self, x: string) -> string { return x }
2524  }
2525  fn run_proc<T>(p: T) where T: Processor { log(p.process(42)) }
2526  run_proc(MyProc({name: "a"}))
2527}"#,
2528        );
2529        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2530        assert!(
2531            warns[0].contains("method 'process' parameter 1 has type 'string', expected 'int'"),
2532            "unexpected message: {}",
2533            warns[0]
2534        );
2535    }
2536
2537    #[test]
2538    fn test_interface_constraint_missing_method() {
2539        let warns = iface_warns(
2540            r#"pipeline t(task) {
2541  interface Sizable {
2542    fn size(self) -> int
2543  }
2544  struct Box { width: int }
2545  impl Box {
2546    fn area(self) -> int { return self.width }
2547  }
2548  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2549  measure(Box({width: 3}))
2550}"#,
2551        );
2552        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2553        assert!(
2554            warns[0].contains("missing method 'size'"),
2555            "unexpected message: {}",
2556            warns[0]
2557        );
2558    }
2559
2560    #[test]
2561    fn test_interface_constraint_param_count_mismatch() {
2562        let warns = iface_warns(
2563            r#"pipeline t(task) {
2564  interface Doubler {
2565    fn double(self, x: int) -> int
2566  }
2567  struct Bad { v: int }
2568  impl Bad {
2569    fn double(self) -> int { return self.v * 2 }
2570  }
2571  fn run_double<T>(d: T) where T: Doubler { log(d.double(3)) }
2572  run_double(Bad({v: 5}))
2573}"#,
2574        );
2575        assert_eq!(warns.len(), 1, "expected 1 warning, got: {:?}", warns);
2576        assert!(
2577            warns[0].contains("method 'double' has 0 parameter(s), expected 1"),
2578            "unexpected message: {}",
2579            warns[0]
2580        );
2581    }
2582
2583    #[test]
2584    fn test_interface_constraint_satisfied() {
2585        let warns = iface_warns(
2586            r#"pipeline t(task) {
2587  interface Sizable {
2588    fn size(self) -> int
2589  }
2590  struct Box { width: int, height: int }
2591  impl Box {
2592    fn size(self) -> int { return self.width * self.height }
2593  }
2594  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2595  measure(Box({width: 3, height: 4}))
2596}"#,
2597        );
2598        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
2599    }
2600
2601    #[test]
2602    fn test_interface_constraint_untyped_impl_compatible() {
2603        // Gradual typing: untyped impl return should not trigger warning
2604        let warns = iface_warns(
2605            r#"pipeline t(task) {
2606  interface Sizable {
2607    fn size(self) -> int
2608  }
2609  struct Box { width: int }
2610  impl Box {
2611    fn size(self) { return self.width }
2612  }
2613  fn measure<T>(item: T) where T: Sizable { log(item.size()) }
2614  measure(Box({width: 3}))
2615}"#,
2616        );
2617        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
2618    }
2619
2620    #[test]
2621    fn test_interface_constraint_int_float_covariance() {
2622        // int is compatible with float (covariance)
2623        let warns = iface_warns(
2624            r#"pipeline t(task) {
2625  interface Measurable {
2626    fn value(self) -> float
2627  }
2628  struct Gauge { v: int }
2629  impl Gauge {
2630    fn value(self) -> int { return self.v }
2631  }
2632  fn read_val<T>(g: T) where T: Measurable { log(g.value()) }
2633  read_val(Gauge({v: 42}))
2634}"#,
2635        );
2636        assert!(warns.is_empty(), "expected no warnings, got: {:?}", warns);
2637    }
2638}