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 stream_fn_depth: usize,
83 stream_emit_types: Vec<Option<TypeExpr>>,
85 deprecated_fns: std::collections::HashMap<String, (Option<String>, Option<String>)>,
90 imported_names: Option<HashSet<String>>,
97 imported_type_decls: Vec<SNode>,
101}
102
103impl TypeChecker {
104 pub(in crate::typechecker) fn wildcard_type() -> TypeExpr {
105 TypeExpr::Named("_".into())
106 }
107
108 pub(in crate::typechecker) fn is_wildcard_type(ty: &TypeExpr) -> bool {
109 matches!(ty, TypeExpr::Named(name) if name == "_")
110 }
111
112 pub(in crate::typechecker) fn base_type_name(ty: &TypeExpr) -> Option<&str> {
113 match ty {
114 TypeExpr::Named(name) => Some(name.as_str()),
115 TypeExpr::Applied { name, .. } => Some(name.as_str()),
116 _ => None,
117 }
118 }
119
120 pub fn new() -> Self {
121 Self {
122 diagnostics: Vec::new(),
123 scope: TypeScope::new(),
124 source: None,
125 hints: Vec::new(),
126 strict_types: false,
127 fn_depth: 0,
128 stream_fn_depth: 0,
129 stream_emit_types: Vec::new(),
130 deprecated_fns: std::collections::HashMap::new(),
131 imported_names: None,
132 imported_type_decls: Vec::new(),
133 }
134 }
135
136 pub fn with_strict_types(strict: bool) -> Self {
139 Self {
140 diagnostics: Vec::new(),
141 scope: TypeScope::new(),
142 source: None,
143 hints: Vec::new(),
144 strict_types: strict,
145 fn_depth: 0,
146 stream_fn_depth: 0,
147 stream_emit_types: Vec::new(),
148 deprecated_fns: std::collections::HashMap::new(),
149 imported_names: None,
150 imported_type_decls: Vec::new(),
151 }
152 }
153
154 pub fn with_imported_names(mut self, imported: HashSet<String>) -> Self {
165 self.imported_names = Some(imported);
166 self
167 }
168
169 pub fn with_imported_type_decls(mut self, imported: Vec<SNode>) -> Self {
173 self.imported_type_decls = imported;
174 self
175 }
176
177 pub fn check_with_source(mut self, program: &[SNode], source: &str) -> Vec<TypeDiagnostic> {
179 self.source = Some(source.to_string());
180 self.check_inner(program).0
181 }
182
183 pub fn check_strict_with_source(
185 mut self,
186 program: &[SNode],
187 source: &str,
188 ) -> Vec<TypeDiagnostic> {
189 self.source = Some(source.to_string());
190 self.check_inner(program).0
191 }
192
193 pub fn check(self, program: &[SNode]) -> Vec<TypeDiagnostic> {
195 self.check_inner(program).0
196 }
197
198 pub(in crate::typechecker) fn detect_boundary_source(
202 value: &SNode,
203 scope: &TypeScope,
204 ) -> Option<String> {
205 match &value.node {
206 Node::FunctionCall { name, args, .. } => {
207 if !builtin_signatures::is_untyped_boundary_source(name) {
208 return None;
209 }
210 if (name == "llm_call" || name == "llm_completion")
212 && Self::llm_call_has_typed_schema_option(args, scope)
213 {
214 return None;
215 }
216 Some(name.clone())
217 }
218 Node::Identifier(name) => scope.is_untyped_source(name).map(|s| s.to_string()),
219 _ => None,
220 }
221 }
222
223 pub(in crate::typechecker) fn llm_call_has_typed_schema_option(
229 args: &[SNode],
230 scope: &TypeScope,
231 ) -> bool {
232 let Some(opts) = args.get(2) else {
233 return false;
234 };
235 let Node::DictLiteral(entries) = &opts.node else {
236 return false;
237 };
238 entries.iter().any(|entry| {
239 let key = match &entry.key.node {
240 Node::StringLiteral(k) | Node::Identifier(k) => k.as_str(),
241 _ => return false,
242 };
243 (key == "schema" || key == "output_schema")
244 && schema_type_expr_from_node(&entry.value, scope).is_some()
245 })
246 }
247
248 pub(in crate::typechecker) fn is_concrete_type(ty: &TypeExpr) -> bool {
251 matches!(
252 ty,
253 TypeExpr::Shape(_)
254 | TypeExpr::Applied { .. }
255 | TypeExpr::FnType { .. }
256 | TypeExpr::List(_)
257 | TypeExpr::Iter(_)
258 | TypeExpr::Generator(_)
259 | TypeExpr::Stream(_)
260 | TypeExpr::DictType(_, _)
261 ) || matches!(ty, TypeExpr::Named(n) if n != "dict" && n != "any" && n != "_")
262 }
263
264 pub fn check_with_hints(
266 mut self,
267 program: &[SNode],
268 source: &str,
269 ) -> (Vec<TypeDiagnostic>, Vec<InlayHintInfo>) {
270 self.source = Some(source.to_string());
271 self.check_inner(program)
272 }
273
274 pub(in crate::typechecker) fn error_at(&mut self, message: String, span: Span) {
275 self.diagnostics.push(TypeDiagnostic {
276 message,
277 severity: DiagnosticSeverity::Error,
278 span: Some(span),
279 help: None,
280 fix: None,
281 details: None,
282 });
283 }
284
285 #[allow(dead_code)]
286 pub(in crate::typechecker) fn error_at_with_help(
287 &mut self,
288 message: String,
289 span: Span,
290 help: String,
291 ) {
292 self.diagnostics.push(TypeDiagnostic {
293 message,
294 severity: DiagnosticSeverity::Error,
295 span: Some(span),
296 help: Some(help),
297 fix: None,
298 details: None,
299 });
300 }
301
302 pub(in crate::typechecker) fn error_at_with_fix(
303 &mut self,
304 message: String,
305 span: Span,
306 fix: Vec<FixEdit>,
307 ) {
308 self.diagnostics.push(TypeDiagnostic {
309 message,
310 severity: DiagnosticSeverity::Error,
311 span: Some(span),
312 help: None,
313 fix: Some(fix),
314 details: None,
315 });
316 }
317
318 pub(in crate::typechecker) fn exhaustiveness_error_at(&mut self, message: String, span: Span) {
325 self.diagnostics.push(TypeDiagnostic {
326 message,
327 severity: DiagnosticSeverity::Error,
328 span: Some(span),
329 help: None,
330 fix: None,
331 details: None,
332 });
333 }
334
335 pub(in crate::typechecker) fn exhaustiveness_error_with_missing(
340 &mut self,
341 message: String,
342 span: Span,
343 missing: Vec<String>,
344 ) {
345 self.diagnostics.push(TypeDiagnostic {
346 message,
347 severity: DiagnosticSeverity::Error,
348 span: Some(span),
349 help: None,
350 fix: None,
351 details: Some(DiagnosticDetails::NonExhaustiveMatch { missing }),
352 });
353 }
354
355 pub(in crate::typechecker) fn warning_at(&mut self, message: String, span: Span) {
356 self.diagnostics.push(TypeDiagnostic {
357 message,
358 severity: DiagnosticSeverity::Warning,
359 span: Some(span),
360 help: None,
361 fix: None,
362 details: None,
363 });
364 }
365
366 #[allow(dead_code)]
367 pub(in crate::typechecker) fn warning_at_with_help(
368 &mut self,
369 message: String,
370 span: Span,
371 help: String,
372 ) {
373 self.diagnostics.push(TypeDiagnostic {
374 message,
375 severity: DiagnosticSeverity::Warning,
376 span: Some(span),
377 help: Some(help),
378 fix: None,
379 details: None,
380 });
381 }
382}
383
384impl Default for TypeChecker {
385 fn default() -> Self {
386 Self::new()
387 }
388}
389
390#[cfg(test)]
391mod tests;