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 pub details: Option<DiagnosticDetails>,
45}
46
47#[derive(Debug, Clone)]
54pub enum DiagnosticDetails {
55 NonExhaustiveMatch { missing: Vec<String> },
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum DiagnosticSeverity {
66 Error,
67 Warning,
68}
69
70pub struct TypeChecker {
72 diagnostics: Vec<TypeDiagnostic>,
73 scope: TypeScope,
74 source: Option<String>,
75 hints: Vec<InlayHintInfo>,
76 strict_types: bool,
78 fn_depth: usize,
81 deprecated_fns: std::collections::HashMap<String, (Option<String>, Option<String>)>,
86 imported_names: Option<HashSet<String>>,
93 imported_type_decls: Vec<SNode>,
97}
98
99impl TypeChecker {
100 pub(in crate::typechecker) fn wildcard_type() -> TypeExpr {
101 TypeExpr::Named("_".into())
102 }
103
104 pub(in crate::typechecker) fn is_wildcard_type(ty: &TypeExpr) -> bool {
105 matches!(ty, TypeExpr::Named(name) if name == "_")
106 }
107
108 pub(in crate::typechecker) fn base_type_name(ty: &TypeExpr) -> Option<&str> {
109 match ty {
110 TypeExpr::Named(name) => Some(name.as_str()),
111 TypeExpr::Applied { name, .. } => Some(name.as_str()),
112 _ => None,
113 }
114 }
115
116 pub fn new() -> Self {
117 Self {
118 diagnostics: Vec::new(),
119 scope: TypeScope::new(),
120 source: None,
121 hints: Vec::new(),
122 strict_types: false,
123 fn_depth: 0,
124 deprecated_fns: std::collections::HashMap::new(),
125 imported_names: None,
126 imported_type_decls: Vec::new(),
127 }
128 }
129
130 pub fn with_strict_types(strict: bool) -> Self {
133 Self {
134 diagnostics: Vec::new(),
135 scope: TypeScope::new(),
136 source: None,
137 hints: Vec::new(),
138 strict_types: strict,
139 fn_depth: 0,
140 deprecated_fns: std::collections::HashMap::new(),
141 imported_names: None,
142 imported_type_decls: Vec::new(),
143 }
144 }
145
146 pub fn with_imported_names(mut self, imported: HashSet<String>) -> Self {
157 self.imported_names = Some(imported);
158 self
159 }
160
161 pub fn with_imported_type_decls(mut self, imported: Vec<SNode>) -> Self {
165 self.imported_type_decls = imported;
166 self
167 }
168
169 pub fn check_with_source(mut self, program: &[SNode], source: &str) -> Vec<TypeDiagnostic> {
171 self.source = Some(source.to_string());
172 self.check_inner(program).0
173 }
174
175 pub fn check_strict_with_source(
177 mut self,
178 program: &[SNode],
179 source: &str,
180 ) -> Vec<TypeDiagnostic> {
181 self.source = Some(source.to_string());
182 self.check_inner(program).0
183 }
184
185 pub fn check(self, program: &[SNode]) -> Vec<TypeDiagnostic> {
187 self.check_inner(program).0
188 }
189
190 pub(in crate::typechecker) fn detect_boundary_source(
194 value: &SNode,
195 scope: &TypeScope,
196 ) -> Option<String> {
197 match &value.node {
198 Node::FunctionCall { name, args } => {
199 if !builtin_signatures::is_untyped_boundary_source(name) {
200 return None;
201 }
202 if (name == "llm_call" || name == "llm_completion")
204 && Self::llm_call_has_typed_schema_option(args, scope)
205 {
206 return None;
207 }
208 Some(name.clone())
209 }
210 Node::Identifier(name) => scope.is_untyped_source(name).map(|s| s.to_string()),
211 _ => None,
212 }
213 }
214
215 pub(in crate::typechecker) fn llm_call_has_typed_schema_option(
221 args: &[SNode],
222 scope: &TypeScope,
223 ) -> bool {
224 let Some(opts) = args.get(2) else {
225 return false;
226 };
227 let Node::DictLiteral(entries) = &opts.node else {
228 return false;
229 };
230 entries.iter().any(|entry| {
231 let key = match &entry.key.node {
232 Node::StringLiteral(k) | Node::Identifier(k) => k.as_str(),
233 _ => return false,
234 };
235 (key == "schema" || key == "output_schema")
236 && schema_type_expr_from_node(&entry.value, scope).is_some()
237 })
238 }
239
240 pub(in crate::typechecker) fn is_concrete_type(ty: &TypeExpr) -> bool {
243 matches!(
244 ty,
245 TypeExpr::Shape(_)
246 | TypeExpr::Applied { .. }
247 | TypeExpr::FnType { .. }
248 | TypeExpr::List(_)
249 | TypeExpr::Iter(_)
250 | TypeExpr::DictType(_, _)
251 ) || matches!(ty, TypeExpr::Named(n) if n != "dict" && n != "any" && n != "_")
252 }
253
254 pub fn check_with_hints(
256 mut self,
257 program: &[SNode],
258 source: &str,
259 ) -> (Vec<TypeDiagnostic>, Vec<InlayHintInfo>) {
260 self.source = Some(source.to_string());
261 self.check_inner(program)
262 }
263
264 pub(in crate::typechecker) fn error_at(&mut self, message: String, span: Span) {
265 self.diagnostics.push(TypeDiagnostic {
266 message,
267 severity: DiagnosticSeverity::Error,
268 span: Some(span),
269 help: None,
270 fix: None,
271 details: None,
272 });
273 }
274
275 #[allow(dead_code)]
276 pub(in crate::typechecker) fn error_at_with_help(
277 &mut self,
278 message: String,
279 span: Span,
280 help: String,
281 ) {
282 self.diagnostics.push(TypeDiagnostic {
283 message,
284 severity: DiagnosticSeverity::Error,
285 span: Some(span),
286 help: Some(help),
287 fix: None,
288 details: None,
289 });
290 }
291
292 pub(in crate::typechecker) fn error_at_with_fix(
293 &mut self,
294 message: String,
295 span: Span,
296 fix: Vec<FixEdit>,
297 ) {
298 self.diagnostics.push(TypeDiagnostic {
299 message,
300 severity: DiagnosticSeverity::Error,
301 span: Some(span),
302 help: None,
303 fix: Some(fix),
304 details: None,
305 });
306 }
307
308 pub(in crate::typechecker) fn exhaustiveness_error_at(&mut self, message: String, span: Span) {
315 self.diagnostics.push(TypeDiagnostic {
316 message,
317 severity: DiagnosticSeverity::Error,
318 span: Some(span),
319 help: None,
320 fix: None,
321 details: None,
322 });
323 }
324
325 pub(in crate::typechecker) fn exhaustiveness_error_with_missing(
330 &mut self,
331 message: String,
332 span: Span,
333 missing: Vec<String>,
334 ) {
335 self.diagnostics.push(TypeDiagnostic {
336 message,
337 severity: DiagnosticSeverity::Error,
338 span: Some(span),
339 help: None,
340 fix: None,
341 details: Some(DiagnosticDetails::NonExhaustiveMatch { missing }),
342 });
343 }
344
345 pub(in crate::typechecker) fn warning_at(&mut self, message: String, span: Span) {
346 self.diagnostics.push(TypeDiagnostic {
347 message,
348 severity: DiagnosticSeverity::Warning,
349 span: Some(span),
350 help: None,
351 fix: None,
352 details: None,
353 });
354 }
355
356 #[allow(dead_code)]
357 pub(in crate::typechecker) fn warning_at_with_help(
358 &mut self,
359 message: String,
360 span: Span,
361 help: String,
362 ) {
363 self.diagnostics.push(TypeDiagnostic {
364 message,
365 severity: DiagnosticSeverity::Warning,
366 span: Some(span),
367 help: Some(help),
368 fix: None,
369 details: None,
370 });
371 }
372}
373
374impl Default for TypeChecker {
375 fn default() -> Self {
376 Self::new()
377 }
378}
379
380#[cfg(test)]
381mod tests;