1use std::collections::HashSet;
2
3use crate::ast::*;
4use crate::builtin_signatures;
5use harn_lexer::{FixEdit, Span};
6
7mod binary_ops;
8mod exits;
9mod format;
10mod inference;
11mod schema_inference;
12mod scope;
13mod union;
14
15pub use exits::{block_definitely_exits, stmt_definitely_exits};
16pub use format::{format_type, shape_mismatch_detail};
17
18use schema_inference::schema_type_expr_from_node;
19use scope::TypeScope;
20
21#[derive(Debug, Clone)]
23pub struct InlayHintInfo {
24 pub line: usize,
26 pub column: usize,
27 pub label: String,
29}
30
31#[derive(Debug, Clone)]
33pub struct TypeDiagnostic {
34 pub message: String,
35 pub severity: DiagnosticSeverity,
36 pub span: Option<Span>,
37 pub help: Option<String>,
38 pub fix: Option<Vec<FixEdit>>,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum DiagnosticSeverity {
44 Error,
45 Warning,
46}
47
48pub struct TypeChecker {
50 diagnostics: Vec<TypeDiagnostic>,
51 scope: TypeScope,
52 source: Option<String>,
53 hints: Vec<InlayHintInfo>,
54 strict_types: bool,
56 fn_depth: usize,
59 deprecated_fns: std::collections::HashMap<String, (Option<String>, Option<String>)>,
64 imported_names: Option<HashSet<String>>,
71}
72
73impl TypeChecker {
74 pub(in crate::typechecker) fn wildcard_type() -> TypeExpr {
75 TypeExpr::Named("_".into())
76 }
77
78 pub(in crate::typechecker) fn is_wildcard_type(ty: &TypeExpr) -> bool {
79 matches!(ty, TypeExpr::Named(name) if name == "_")
80 }
81
82 pub(in crate::typechecker) fn base_type_name(ty: &TypeExpr) -> Option<&str> {
83 match ty {
84 TypeExpr::Named(name) => Some(name.as_str()),
85 TypeExpr::Applied { name, .. } => Some(name.as_str()),
86 _ => None,
87 }
88 }
89
90 pub fn new() -> Self {
91 Self {
92 diagnostics: Vec::new(),
93 scope: TypeScope::new(),
94 source: None,
95 hints: Vec::new(),
96 strict_types: false,
97 fn_depth: 0,
98 deprecated_fns: std::collections::HashMap::new(),
99 imported_names: None,
100 }
101 }
102
103 pub fn with_strict_types(strict: bool) -> Self {
106 Self {
107 diagnostics: Vec::new(),
108 scope: TypeScope::new(),
109 source: None,
110 hints: Vec::new(),
111 strict_types: strict,
112 fn_depth: 0,
113 deprecated_fns: std::collections::HashMap::new(),
114 imported_names: None,
115 }
116 }
117
118 pub fn with_imported_names(mut self, imported: HashSet<String>) -> Self {
129 self.imported_names = Some(imported);
130 self
131 }
132
133 pub fn check_with_source(mut self, program: &[SNode], source: &str) -> Vec<TypeDiagnostic> {
135 self.source = Some(source.to_string());
136 self.check_inner(program).0
137 }
138
139 pub fn check_strict_with_source(
141 mut self,
142 program: &[SNode],
143 source: &str,
144 ) -> Vec<TypeDiagnostic> {
145 self.source = Some(source.to_string());
146 self.check_inner(program).0
147 }
148
149 pub fn check(self, program: &[SNode]) -> Vec<TypeDiagnostic> {
151 self.check_inner(program).0
152 }
153
154 pub(in crate::typechecker) fn detect_boundary_source(
158 value: &SNode,
159 scope: &TypeScope,
160 ) -> Option<String> {
161 match &value.node {
162 Node::FunctionCall { name, args } => {
163 if !builtin_signatures::is_untyped_boundary_source(name) {
164 return None;
165 }
166 if (name == "llm_call" || name == "llm_completion")
168 && Self::llm_call_has_typed_schema_option(args, scope)
169 {
170 return None;
171 }
172 Some(name.clone())
173 }
174 Node::Identifier(name) => scope.is_untyped_source(name).map(|s| s.to_string()),
175 _ => None,
176 }
177 }
178
179 pub(in crate::typechecker) fn llm_call_has_typed_schema_option(
185 args: &[SNode],
186 scope: &TypeScope,
187 ) -> bool {
188 let Some(opts) = args.get(2) else {
189 return false;
190 };
191 let Node::DictLiteral(entries) = &opts.node else {
192 return false;
193 };
194 entries.iter().any(|entry| {
195 let key = match &entry.key.node {
196 Node::StringLiteral(k) | Node::Identifier(k) => k.as_str(),
197 _ => return false,
198 };
199 (key == "schema" || key == "output_schema")
200 && schema_type_expr_from_node(&entry.value, scope).is_some()
201 })
202 }
203
204 pub(in crate::typechecker) fn is_concrete_type(ty: &TypeExpr) -> bool {
207 matches!(
208 ty,
209 TypeExpr::Shape(_)
210 | TypeExpr::Applied { .. }
211 | TypeExpr::FnType { .. }
212 | TypeExpr::List(_)
213 | TypeExpr::Iter(_)
214 | TypeExpr::DictType(_, _)
215 ) || matches!(ty, TypeExpr::Named(n) if n != "dict" && n != "any" && n != "_")
216 }
217
218 pub fn check_with_hints(
220 mut self,
221 program: &[SNode],
222 source: &str,
223 ) -> (Vec<TypeDiagnostic>, Vec<InlayHintInfo>) {
224 self.source = Some(source.to_string());
225 self.check_inner(program)
226 }
227
228 pub(in crate::typechecker) fn error_at(&mut self, message: String, span: Span) {
229 self.diagnostics.push(TypeDiagnostic {
230 message,
231 severity: DiagnosticSeverity::Error,
232 span: Some(span),
233 help: None,
234 fix: None,
235 });
236 }
237
238 #[allow(dead_code)]
239 pub(in crate::typechecker) fn error_at_with_help(
240 &mut self,
241 message: String,
242 span: Span,
243 help: String,
244 ) {
245 self.diagnostics.push(TypeDiagnostic {
246 message,
247 severity: DiagnosticSeverity::Error,
248 span: Some(span),
249 help: Some(help),
250 fix: None,
251 });
252 }
253
254 pub(in crate::typechecker) fn error_at_with_fix(
255 &mut self,
256 message: String,
257 span: Span,
258 fix: Vec<FixEdit>,
259 ) {
260 self.diagnostics.push(TypeDiagnostic {
261 message,
262 severity: DiagnosticSeverity::Error,
263 span: Some(span),
264 help: None,
265 fix: Some(fix),
266 });
267 }
268
269 pub(in crate::typechecker) fn warning_at(&mut self, message: String, span: Span) {
270 self.diagnostics.push(TypeDiagnostic {
271 message,
272 severity: DiagnosticSeverity::Warning,
273 span: Some(span),
274 help: None,
275 fix: None,
276 });
277 }
278
279 #[allow(dead_code)]
280 pub(in crate::typechecker) fn warning_at_with_help(
281 &mut self,
282 message: String,
283 span: Span,
284 help: String,
285 ) {
286 self.diagnostics.push(TypeDiagnostic {
287 message,
288 severity: DiagnosticSeverity::Warning,
289 span: Some(span),
290 help: Some(help),
291 fix: None,
292 });
293 }
294}
295
296impl Default for TypeChecker {
297 fn default() -> Self {
298 Self::new()
299 }
300}
301
302#[cfg(test)]
303mod tests;