Skip to main content

harn_parser/
typechecker.rs

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