Skip to main content

harn_parser/
typechecker.rs

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