Skip to main content

harn_parser/
typechecker.rs

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