use std::collections::HashSet;
use crate::ast::*;
use crate::builtin_signatures;
use harn_lexer::{FixEdit, Span};
mod binary_ops;
mod exits;
mod format;
mod inference;
mod schema_inference;
mod scope;
mod union;
pub use exits::{block_definitely_exits, stmt_definitely_exits};
pub use format::{format_type, shape_mismatch_detail};
use schema_inference::schema_type_expr_from_node;
use scope::TypeScope;
#[derive(Debug, Clone)]
pub struct InlayHintInfo {
pub line: usize,
pub column: usize,
pub label: String,
}
#[derive(Debug, Clone)]
pub struct TypeDiagnostic {
pub message: String,
pub severity: DiagnosticSeverity,
pub span: Option<Span>,
pub help: Option<String>,
pub fix: Option<Vec<FixEdit>>,
pub details: Option<DiagnosticDetails>,
}
#[derive(Debug, Clone)]
pub enum DiagnosticDetails {
NonExhaustiveMatch { missing: Vec<String> },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticSeverity {
Error,
Warning,
}
pub struct TypeChecker {
diagnostics: Vec<TypeDiagnostic>,
scope: TypeScope,
source: Option<String>,
hints: Vec<InlayHintInfo>,
strict_types: bool,
fn_depth: usize,
deprecated_fns: std::collections::HashMap<String, (Option<String>, Option<String>)>,
imported_names: Option<HashSet<String>>,
}
impl TypeChecker {
pub(in crate::typechecker) fn wildcard_type() -> TypeExpr {
TypeExpr::Named("_".into())
}
pub(in crate::typechecker) fn is_wildcard_type(ty: &TypeExpr) -> bool {
matches!(ty, TypeExpr::Named(name) if name == "_")
}
pub(in crate::typechecker) fn base_type_name(ty: &TypeExpr) -> Option<&str> {
match ty {
TypeExpr::Named(name) => Some(name.as_str()),
TypeExpr::Applied { name, .. } => Some(name.as_str()),
_ => None,
}
}
pub fn new() -> Self {
Self {
diagnostics: Vec::new(),
scope: TypeScope::new(),
source: None,
hints: Vec::new(),
strict_types: false,
fn_depth: 0,
deprecated_fns: std::collections::HashMap::new(),
imported_names: None,
}
}
pub fn with_strict_types(strict: bool) -> Self {
Self {
diagnostics: Vec::new(),
scope: TypeScope::new(),
source: None,
hints: Vec::new(),
strict_types: strict,
fn_depth: 0,
deprecated_fns: std::collections::HashMap::new(),
imported_names: None,
}
}
pub fn with_imported_names(mut self, imported: HashSet<String>) -> Self {
self.imported_names = Some(imported);
self
}
pub fn check_with_source(mut self, program: &[SNode], source: &str) -> Vec<TypeDiagnostic> {
self.source = Some(source.to_string());
self.check_inner(program).0
}
pub fn check_strict_with_source(
mut self,
program: &[SNode],
source: &str,
) -> Vec<TypeDiagnostic> {
self.source = Some(source.to_string());
self.check_inner(program).0
}
pub fn check(self, program: &[SNode]) -> Vec<TypeDiagnostic> {
self.check_inner(program).0
}
pub(in crate::typechecker) fn detect_boundary_source(
value: &SNode,
scope: &TypeScope,
) -> Option<String> {
match &value.node {
Node::FunctionCall { name, args } => {
if !builtin_signatures::is_untyped_boundary_source(name) {
return None;
}
if (name == "llm_call" || name == "llm_completion")
&& Self::llm_call_has_typed_schema_option(args, scope)
{
return None;
}
Some(name.clone())
}
Node::Identifier(name) => scope.is_untyped_source(name).map(|s| s.to_string()),
_ => None,
}
}
pub(in crate::typechecker) fn llm_call_has_typed_schema_option(
args: &[SNode],
scope: &TypeScope,
) -> bool {
let Some(opts) = args.get(2) else {
return false;
};
let Node::DictLiteral(entries) = &opts.node else {
return false;
};
entries.iter().any(|entry| {
let key = match &entry.key.node {
Node::StringLiteral(k) | Node::Identifier(k) => k.as_str(),
_ => return false,
};
(key == "schema" || key == "output_schema")
&& schema_type_expr_from_node(&entry.value, scope).is_some()
})
}
pub(in crate::typechecker) fn is_concrete_type(ty: &TypeExpr) -> bool {
matches!(
ty,
TypeExpr::Shape(_)
| TypeExpr::Applied { .. }
| TypeExpr::FnType { .. }
| TypeExpr::List(_)
| TypeExpr::Iter(_)
| TypeExpr::DictType(_, _)
) || matches!(ty, TypeExpr::Named(n) if n != "dict" && n != "any" && n != "_")
}
pub fn check_with_hints(
mut self,
program: &[SNode],
source: &str,
) -> (Vec<TypeDiagnostic>, Vec<InlayHintInfo>) {
self.source = Some(source.to_string());
self.check_inner(program)
}
pub(in crate::typechecker) fn error_at(&mut self, message: String, span: Span) {
self.diagnostics.push(TypeDiagnostic {
message,
severity: DiagnosticSeverity::Error,
span: Some(span),
help: None,
fix: None,
details: None,
});
}
#[allow(dead_code)]
pub(in crate::typechecker) fn error_at_with_help(
&mut self,
message: String,
span: Span,
help: String,
) {
self.diagnostics.push(TypeDiagnostic {
message,
severity: DiagnosticSeverity::Error,
span: Some(span),
help: Some(help),
fix: None,
details: None,
});
}
pub(in crate::typechecker) fn error_at_with_fix(
&mut self,
message: String,
span: Span,
fix: Vec<FixEdit>,
) {
self.diagnostics.push(TypeDiagnostic {
message,
severity: DiagnosticSeverity::Error,
span: Some(span),
help: None,
fix: Some(fix),
details: None,
});
}
pub(in crate::typechecker) fn exhaustiveness_error_at(&mut self, message: String, span: Span) {
self.diagnostics.push(TypeDiagnostic {
message,
severity: DiagnosticSeverity::Error,
span: Some(span),
help: None,
fix: None,
details: None,
});
}
pub(in crate::typechecker) fn exhaustiveness_error_with_missing(
&mut self,
message: String,
span: Span,
missing: Vec<String>,
) {
self.diagnostics.push(TypeDiagnostic {
message,
severity: DiagnosticSeverity::Error,
span: Some(span),
help: None,
fix: None,
details: Some(DiagnosticDetails::NonExhaustiveMatch { missing }),
});
}
pub(in crate::typechecker) fn warning_at(&mut self, message: String, span: Span) {
self.diagnostics.push(TypeDiagnostic {
message,
severity: DiagnosticSeverity::Warning,
span: Some(span),
help: None,
fix: None,
details: None,
});
}
#[allow(dead_code)]
pub(in crate::typechecker) fn warning_at_with_help(
&mut self,
message: String,
span: Span,
help: String,
) {
self.diagnostics.push(TypeDiagnostic {
message,
severity: DiagnosticSeverity::Warning,
span: Some(span),
help: Some(help),
fix: None,
details: None,
});
}
}
impl Default for TypeChecker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests;