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