Files
lux/src/typechecker.rs
Brandon Lucas 62be78ff99 feat: implement better error messages with 'Did you mean?' suggestions
Add Levenshtein distance-based similarity matching for undefined
variables, unknown types, unknown effects, and unknown traits.
When a name is not found, the error now suggests similar names
within edit distance 2.

Changes:
- Add levenshtein_distance() function to diagnostics module
- Add find_similar_names() and format_did_you_mean() helpers
- Update typechecker to suggest similar names for:
  - Undefined variables
  - Unknown types
  - Unknown effects
  - Unknown traits
- Add 17 new tests for similarity matching

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-13 09:39:26 -05:00

1816 lines
68 KiB
Rust

//! Type checker for the Lux language
#![allow(dead_code, unused_variables)]
use std::collections::HashMap;
use crate::ast::{
self, BinaryOp, Declaration, EffectDecl, Expr, FunctionDecl, HandlerDecl, Ident, ImplDecl,
ImportDecl, LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program, Span,
Statement, TraitDecl, TypeDecl, TypeExpr, UnaryOp, VariantFields,
};
use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, Severity};
use crate::exhaustiveness::{check_exhaustiveness, missing_patterns_hint};
use crate::modules::ModuleLoader;
use crate::types::{
self, unify, EffectDef, EffectOpDef, EffectSet, HandlerDef, PropertySet, TraitBoundDef,
TraitDef, TraitImpl, TraitMethodDef, Type, TypeEnv, TypeScheme, VariantDef, VariantFieldsDef,
};
/// Type checking error
#[derive(Debug, Clone)]
pub struct TypeError {
pub message: String,
pub span: Span,
}
impl std::fmt::Display for TypeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Type error at {}-{}: {}",
self.span.start, self.span.end, self.message
)
}
}
impl TypeError {
/// Convert to a rich diagnostic for Elm-style error display
pub fn to_diagnostic(&self) -> Diagnostic {
// Categorize the error and extract hints
let (title, hints) = categorize_type_error(&self.message);
Diagnostic {
severity: Severity::Error,
title,
message: self.message.clone(),
span: self.span,
hints,
}
}
}
/// Categorize a type error message to provide better titles and hints
fn categorize_type_error(message: &str) -> (String, Vec<String>) {
let message_lower = message.to_lowercase();
if message_lower.contains("type mismatch") {
(
"Type Mismatch".to_string(),
vec!["Check that the types on both sides of the expression are compatible.".to_string()],
)
} else if message_lower.contains("undefined variable") || message_lower.contains("not found") {
(
"Unknown Name".to_string(),
vec![
"Check the spelling of the name.".to_string(),
"Make sure the variable is defined before use.".to_string(),
],
)
} else if message_lower.contains("cannot unify") {
(
"Type Mismatch".to_string(),
vec!["The types are not compatible. Check your function arguments and return types.".to_string()],
)
} else if message_lower.contains("expected") && message_lower.contains("argument") {
(
"Wrong Number of Arguments".to_string(),
vec!["Check the function signature and provide the correct number of arguments.".to_string()],
)
} else if message_lower.contains("pure") && message_lower.contains("effect") {
(
"Purity Violation".to_string(),
vec![
"Functions marked 'is pure' cannot perform effects.".to_string(),
"Remove the 'is pure' annotation or handle the effects.".to_string(),
],
)
} else if message_lower.contains("effect") && message_lower.contains("unhandled") {
(
"Unhandled Effect".to_string(),
vec!["Use a 'handle' expression to provide an implementation for this effect.".to_string()],
)
} else if message_lower.contains("recursive") {
(
"Invalid Recursion".to_string(),
vec!["Check that recursive calls have proper base cases.".to_string()],
)
} else {
("Type Error".to_string(), vec![])
}
}
/// Type checker
pub struct TypeChecker {
env: TypeEnv,
current_effects: EffectSet,
/// Effects inferred from the current function body (for effect inference)
inferred_effects: EffectSet,
/// Whether we're inferring effects (no explicit declaration)
inferring_effects: bool,
errors: Vec<TypeError>,
/// Type parameters in scope (maps "T" -> Type::Var(n) for generics)
type_params: HashMap<String, Type>,
}
impl TypeChecker {
pub fn new() -> Self {
Self {
env: TypeEnv::with_builtins(),
current_effects: EffectSet::empty(),
inferred_effects: EffectSet::empty(),
inferring_effects: false,
errors: Vec::new(),
type_params: HashMap::new(),
}
}
/// Look up a type scheme by name (for REPL :info command)
pub fn lookup(&self, name: &str) -> Option<&TypeScheme> {
self.env.bindings.get(name)
}
/// Type check a program
pub fn check_program(&mut self, program: &Program) -> Result<(), Vec<TypeError>> {
// First pass: collect all declarations
for decl in &program.declarations {
self.collect_declaration(decl);
}
// Second pass: type check all declarations
for decl in &program.declarations {
self.check_declaration(decl);
}
if self.errors.is_empty() {
Ok(())
} else {
Err(self.errors.clone())
}
}
/// Type check a program with module support
pub fn check_program_with_modules(
&mut self,
program: &Program,
loader: &ModuleLoader,
) -> Result<(), Vec<TypeError>> {
// First, process imports
self.load_type_imports(&program.imports, loader);
// Then check the program normally
self.check_program(program)
}
/// Load type bindings from imported modules
fn load_type_imports(&mut self, imports: &[ImportDecl], loader: &ModuleLoader) {
use crate::modules::ImportKind;
let resolved = match loader.resolve_imports(imports) {
Ok(r) => r,
Err(e) => {
self.errors.push(TypeError {
message: format!("Import error: {}", e.message),
span: Span { start: 0, end: 0 },
});
return;
}
};
for (name, import) in resolved {
match import.kind {
ImportKind::Module => {
// Import as a module object - create a record type
let module = match loader.get_module(&import.module_path) {
Some(m) => m,
None => continue,
};
// Create a temporary checker to get types from the module
let mut module_checker = TypeChecker::new();
for decl in &module.program.declarations {
module_checker.collect_declaration(decl);
}
// Build a record type with all exported names
let mut fields = Vec::new();
for export_name in &module.exports {
if let Some(scheme) = module_checker.env.lookup(export_name) {
fields.push((export_name.clone(), scheme.instantiate()));
}
}
if !fields.is_empty() {
self.env.bind(&name, TypeScheme::mono(Type::Record(fields)));
}
}
ImportKind::Direct => {
// Import a specific name directly
let module = match loader.get_module(&import.module_path) {
Some(m) => m,
None => continue,
};
// Get the type for this specific export
let mut module_checker = TypeChecker::new();
for decl in &module.program.declarations {
module_checker.collect_declaration(decl);
}
if let Some(scheme) = module_checker.env.lookup(&import.name) {
self.env.bind(&name, scheme.clone());
}
// Also copy effects if the imported name is an effect
if let Some(effect) = module_checker.env.lookup_effect(&import.name) {
self.env.effects.insert(name.clone(), effect.clone());
}
// Also copy types if the imported name is a type
if let Some(type_def) = module_checker.env.types.get(&import.name) {
self.env.types.insert(name.clone(), type_def.clone());
}
}
}
}
}
/// Collect type signatures from declarations (first pass)
fn collect_declaration(&mut self, decl: &Declaration) {
match decl {
Declaration::Function(func) => {
let scheme = self.function_type(func);
self.env.bind(&func.name.name, scheme);
}
Declaration::Effect(effect) => {
let effect_def = self.effect_def(effect);
self.env
.effects
.insert(effect.name.name.clone(), effect_def);
}
Declaration::Type(type_decl) => {
// Save old type params for this scope
let old_params = std::mem::take(&mut self.type_params);
// Bind type parameters to fresh type variables
let mut bound_vars = Vec::new();
for param in &type_decl.type_params {
let var = Type::var();
if let Type::Var(n) = &var {
bound_vars.push(*n);
}
self.type_params.insert(param.name.clone(), var);
}
// Build the parameterized return type
let base_type = if type_decl.type_params.is_empty() {
Type::Named(type_decl.name.name.clone())
} else {
Type::App {
constructor: Box::new(Type::Named(type_decl.name.name.clone())),
args: type_decl
.type_params
.iter()
.map(|p| self.type_params.get(&p.name).unwrap().clone())
.collect(),
}
};
// Register the type definition
let type_def = self.type_def(type_decl);
self.env.types.insert(type_decl.name.name.clone(), type_def.clone());
// Register ADT constructors as values with polymorphic types
if let ast::TypeDef::Enum(variants) = &type_decl.definition {
for variant in variants {
let constructor_type = match &variant.fields {
VariantFields::Unit => {
// Unit variant is just the type itself
base_type.clone()
}
VariantFields::Tuple(field_types) => {
// Tuple variant is a function from fields to the type
let param_types: Vec<Type> = field_types
.iter()
.map(|t| self.resolve_type(t))
.collect();
Type::function(param_types, base_type.clone())
}
VariantFields::Record(fields) => {
// Record variant is a function from record to the type
let field_types: Vec<(String, Type)> = fields
.iter()
.map(|f| (f.name.name.clone(), self.resolve_type(&f.typ)))
.collect();
Type::function(vec![Type::Record(field_types)], base_type.clone())
}
};
// Wrap in polymorphic TypeScheme for generic types
let scheme = TypeScheme {
vars: bound_vars.clone(),
typ: constructor_type,
};
self.env.bind(&variant.name.name, scheme);
}
}
// Restore old type params
self.type_params = old_params;
}
Declaration::Handler(handler) => {
let handler_def = self.handler_def(handler);
self.env
.handlers
.insert(handler.name.name.clone(), handler_def);
// Also bind the handler as a value so it can be referenced in run...with expressions
// Handler type is the effect it handles (as an opaque type for now)
let handler_type = Type::Named(format!("Handler<{}>", handler.effect.name));
self.env.bind(&handler.name.name, TypeScheme::mono(handler_type));
}
Declaration::Let(let_decl) => {
// Will be typed in second pass
let typ = if let Some(ref type_expr) = let_decl.typ {
self.resolve_type(type_expr)
} else {
Type::var()
};
self.env.bind(&let_decl.name.name, TypeScheme::mono(typ));
}
Declaration::Trait(trait_decl) => {
let trait_def = self.trait_def(trait_decl);
self.env.traits.insert(trait_decl.name.name.clone(), trait_def);
}
Declaration::Impl(impl_decl) => {
// Will be checked in second pass
let trait_impl = self.collect_impl(impl_decl);
self.env.trait_impls.push(trait_impl);
}
}
}
/// Type check a declaration (second pass)
fn check_declaration(&mut self, decl: &Declaration) {
match decl {
Declaration::Function(func) => {
self.check_function(func);
}
Declaration::Let(let_decl) => {
self.check_let_decl(let_decl);
}
Declaration::Handler(handler) => {
self.check_handler(handler);
}
Declaration::Impl(impl_decl) => {
self.check_impl(impl_decl);
}
// Effects, types, and traits don't need checking beyond collection
_ => {}
}
}
fn check_function(&mut self, func: &FunctionDecl) {
// Set up the environment with parameters
let mut local_env = self.env.clone();
for param in &func.params {
let param_type = self.resolve_type(&param.typ);
local_env.bind(&param.name.name, TypeScheme::mono(param_type));
}
// Determine if we need to infer effects
let explicit_effects = !func.effects.is_empty();
let declared_effects = EffectSet::from_iter(func.effects.iter().map(|e| e.name.clone()));
// Save old state
let old_effects = std::mem::replace(&mut self.current_effects, declared_effects.clone());
let old_inferring = std::mem::replace(&mut self.inferring_effects, !explicit_effects);
let old_inferred = std::mem::replace(&mut self.inferred_effects, EffectSet::empty());
// Type check the body
let old_env = std::mem::replace(&mut self.env, local_env);
let body_type = self.infer_expr(&func.body);
self.env = old_env;
// Get the inferred effects before restoring state
let inferred = std::mem::replace(&mut self.inferred_effects, old_inferred);
// Restore state
self.current_effects = old_effects;
self.inferring_effects = old_inferring;
// Check that body type matches return type
let return_type = self.resolve_type(&func.return_type);
if let Err(e) = unify(&body_type, &return_type) {
self.errors.push(TypeError {
message: format!(
"Function '{}' body has type {}, but declared return type is {}: {}",
func.name.name, body_type, return_type, e
),
span: func.span,
});
}
// Check behavioral properties
let properties = PropertySet::from_ast(&func.properties);
// Pure functions cannot have effects
let effective_effects = if explicit_effects {
&declared_effects
} else {
&inferred
};
if properties.is_pure() && !effective_effects.is_empty() {
let effects_str = if explicit_effects {
func.effects
.iter()
.map(|e| e.name.as_str())
.collect::<Vec<_>>()
.join(", ")
} else {
format!("{} (inferred)", inferred)
};
self.errors.push(TypeError {
message: format!(
"Function '{}' is declared as pure but has effects: {{{}}}",
func.name.name, effects_str
),
span: func.span,
});
}
// If effects were declared, verify that inferred effects are a subset
if explicit_effects && !inferred.is_subset(&declared_effects) {
let missing: Vec<_> = inferred
.effects
.iter()
.filter(|e| !declared_effects.contains(e))
.cloned()
.collect();
self.errors.push(TypeError {
message: format!(
"Function '{}' uses effects {{{}}} but only declares {{{}}}",
func.name.name,
missing.join(", "),
declared_effects
),
span: func.span,
});
}
// Check where clause property constraints
for where_clause in &func.where_clauses {
match where_clause {
ast::WhereClause::PropertyConstraint {
type_param,
property,
span,
} => {
// Record the constraint for later checking when the function is called
// For now, we just validate that the type parameter exists
if !func.type_params.iter().any(|p| p.name == type_param.name)
&& !func.params.iter().any(|p| p.name.name == type_param.name)
{
self.errors.push(TypeError {
message: format!(
"Unknown type parameter '{}' in where clause",
type_param.name
),
span: *span,
});
}
}
ast::WhereClause::ResultRefinement { predicate, span } => {
// Result refinements are checked at runtime or via SMT solver
// For now, we just type-check the predicate expression
// (would need 'result' in scope, which we don't have yet)
}
ast::WhereClause::TraitConstraint(constraint) => {
// Validate that the type parameter exists
if !func.type_params.iter().any(|p| p.name == constraint.type_param.name)
&& !func.params.iter().any(|p| p.name.name == constraint.type_param.name)
{
self.errors.push(TypeError {
message: format!(
"Unknown type parameter '{}' in where clause",
constraint.type_param.name
),
span: constraint.span,
});
}
// Validate that each trait in the bounds exists
for bound in &constraint.bounds {
if !self.env.traits.contains_key(&bound.trait_name.name) {
// Find similar trait names for suggestion
let available_traits: Vec<&str> = self.env.traits.keys()
.map(|s| s.as_str())
.collect();
let suggestions = find_similar_names(&bound.trait_name.name, available_traits, 2);
let mut message = format!("Unknown trait: {}", bound.trait_name.name);
if let Some(hint) = format_did_you_mean(&suggestions) {
message.push_str(&format!(". {}", hint));
}
self.errors.push(TypeError {
message,
span: bound.span,
});
}
}
}
}
}
}
fn check_let_decl(&mut self, let_decl: &LetDecl) {
let inferred = self.infer_expr(&let_decl.value);
if let Some(ref type_expr) = let_decl.typ {
let declared = self.resolve_type(type_expr);
if let Err(e) = unify(&inferred, &declared) {
self.errors.push(TypeError {
message: format!(
"Variable '{}' has type {}, but declared type is {}: {}",
let_decl.name.name, inferred, declared, e
),
span: let_decl.span,
});
}
}
// Update the binding with the inferred type
let scheme = self.env.generalize(&inferred);
self.env.bind(&let_decl.name.name, scheme);
}
fn check_handler(&mut self, handler: &HandlerDecl) {
// Look up the effect being handled
let effect = match self.env.lookup_effect(&handler.effect.name) {
Some(e) => e.clone(),
None => {
// Find similar effect names for suggestion
let available_effects: Vec<&str> = self.env.effects.keys()
.map(|s| s.as_str())
.collect();
let suggestions = find_similar_names(&handler.effect.name, available_effects, 2);
let mut message = format!("Unknown effect: {}", handler.effect.name);
if let Some(hint) = format_did_you_mean(&suggestions) {
message.push_str(&format!(". {}", hint));
}
self.errors.push(TypeError {
message,
span: handler.effect.span,
});
return;
}
};
// Check each implementation
for impl_ in &handler.implementations {
let op = match effect
.operations
.iter()
.find(|o| o.name == impl_.op_name.name)
{
Some(o) => o.clone(),
None => {
self.errors.push(TypeError {
message: format!(
"Effect '{}' has no operation '{}'",
handler.effect.name, impl_.op_name.name
),
span: impl_.op_name.span,
});
continue;
}
};
// Set up environment with operation parameters
let mut local_env = self.env.clone();
for (i, param_name) in impl_.params.iter().enumerate() {
if i < op.params.len() {
local_env.bind(&param_name.name, TypeScheme::mono(op.params[i].1.clone()));
}
}
// Add resume if present
if let Some(ref resume) = impl_.resume {
// resume has type: fn(return_type) -> result_type
let resume_type = Type::function(vec![op.return_type.clone()], Type::var());
local_env.bind(&resume.name, TypeScheme::mono(resume_type));
}
// Type check the implementation body
let old_env = std::mem::replace(&mut self.env, local_env);
let _body_type = self.infer_expr(&impl_.body);
self.env = old_env;
}
}
/// Infer the type of an expression
fn infer_expr(&mut self, expr: &Expr) -> Type {
match expr {
Expr::Literal(lit) => self.infer_literal(lit),
Expr::Var(ident) => match self.env.lookup(&ident.name) {
Some(scheme) => scheme.instantiate(),
None => {
// Find similar variable names for "Did you mean?" suggestion
let available_names: Vec<&str> = self.env.bindings.keys()
.map(|s| s.as_str())
.collect();
let suggestions = find_similar_names(&ident.name, available_names, 2);
let mut message = format!("Undefined variable: {}", ident.name);
if let Some(hint) = format_did_you_mean(&suggestions) {
message.push_str(&format!(". {}", hint));
}
self.errors.push(TypeError {
message,
span: ident.span,
});
Type::Error
}
},
Expr::BinaryOp {
op,
left,
right,
span,
} => self.infer_binary_op(*op, left, right, *span),
Expr::UnaryOp { op, operand, span } => self.infer_unary_op(*op, operand, *span),
Expr::Call { func, args, span } => self.infer_call(func, args, *span),
Expr::EffectOp {
effect,
operation,
args,
span,
} => self.infer_effect_op(effect, operation, args, *span),
Expr::Field {
object,
field,
span,
} => self.infer_field(object, field, *span),
Expr::Lambda {
params,
return_type,
effects,
body,
span,
} => self.infer_lambda(params, return_type.as_deref(), effects, body, *span),
Expr::Let {
name,
typ,
value,
body,
span,
} => self.infer_let(name, typ.as_ref(), value, body, *span),
Expr::If {
condition,
then_branch,
else_branch,
span,
} => self.infer_if(condition, then_branch, else_branch, *span),
Expr::Match {
scrutinee,
arms,
span,
} => self.infer_match(scrutinee, arms, *span),
Expr::Block {
statements,
result,
span,
} => self.infer_block(statements, result, *span),
Expr::Record { fields, span } => self.infer_record(fields, *span),
Expr::Tuple { elements, span } => self.infer_tuple(elements, *span),
Expr::List { elements, span } => self.infer_list(elements, *span),
Expr::Run {
expr,
handlers,
span,
} => self.infer_run(expr, handlers, *span),
Expr::Resume { value, span } => {
// Resume is special - it continues the computation
// For now, just infer the value type
let _ = self.infer_expr(value);
Type::var() // Resume's type depends on context
}
}
}
fn infer_literal(&self, lit: &Literal) -> Type {
match &lit.kind {
LiteralKind::Int(_) => Type::Int,
LiteralKind::Float(_) => Type::Float,
LiteralKind::String(_) => Type::String,
LiteralKind::Char(_) => Type::Char,
LiteralKind::Bool(_) => Type::Bool,
LiteralKind::Unit => Type::Unit,
}
}
fn infer_binary_op(&mut self, op: BinaryOp, left: &Expr, right: &Expr, span: Span) -> Type {
let left_type = self.infer_expr(left);
let right_type = self.infer_expr(right);
match op {
BinaryOp::Add => {
// Add supports both numeric types and string concatenation
if let Err(e) = unify(&left_type, &right_type) {
self.errors.push(TypeError {
message: format!("Operands of '{}' must have same type: {}", op, e),
span,
});
}
match &left_type {
Type::Int | Type::Float | Type::String | Type::Var(_) => left_type,
_ => {
self.errors.push(TypeError {
message: format!(
"Operator '{}' requires numeric or string operands, got {}",
op, left_type
),
span,
});
Type::Error
}
}
}
BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => {
// Arithmetic: both operands must be same numeric type
if let Err(e) = unify(&left_type, &right_type) {
self.errors.push(TypeError {
message: format!("Operands of '{}' must have same type: {}", op, e),
span,
});
}
// Check that they're numeric
match &left_type {
Type::Int | Type::Float | Type::Var(_) => left_type,
_ => {
self.errors.push(TypeError {
message: format!(
"Operator '{}' requires numeric operands, got {}",
op, left_type
),
span,
});
Type::Error
}
}
}
BinaryOp::Eq | BinaryOp::Ne => {
// Equality: operands must have same type
if let Err(e) = unify(&left_type, &right_type) {
self.errors.push(TypeError {
message: format!("Operands of '{}' must have same type: {}", op, e),
span,
});
}
Type::Bool
}
BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => {
// Comparison: operands must be same orderable type
if let Err(e) = unify(&left_type, &right_type) {
self.errors.push(TypeError {
message: format!("Operands of '{}' must have same type: {}", op, e),
span,
});
}
Type::Bool
}
BinaryOp::And | BinaryOp::Or => {
// Logical: both must be Bool
if let Err(e) = unify(&left_type, &Type::Bool) {
self.errors.push(TypeError {
message: format!("Left operand of '{}' must be Bool: {}", op, e),
span: left.span(),
});
}
if let Err(e) = unify(&right_type, &Type::Bool) {
self.errors.push(TypeError {
message: format!("Right operand of '{}' must be Bool: {}", op, e),
span: right.span(),
});
}
Type::Bool
}
BinaryOp::Pipe => {
// Pipe: a |> f means f(a)
// right must be a function that accepts left's type
let result_type = Type::var();
let expected_fn = Type::function(vec![left_type.clone()], result_type.clone());
if let Err(e) = unify(&right_type, &expected_fn) {
self.errors.push(TypeError {
message: format!(
"Pipe target must be a function accepting {}: {}",
left_type, e
),
span,
});
}
result_type
}
}
}
fn infer_unary_op(&mut self, op: UnaryOp, operand: &Expr, span: Span) -> Type {
let operand_type = self.infer_expr(operand);
match op {
UnaryOp::Neg => match &operand_type {
Type::Int | Type::Float | Type::Var(_) => operand_type,
_ => {
self.errors.push(TypeError {
message: format!(
"Operator '-' requires numeric operand, got {}",
operand_type
),
span,
});
Type::Error
}
},
UnaryOp::Not => {
if let Err(e) = unify(&operand_type, &Type::Bool) {
self.errors.push(TypeError {
message: format!("Operator '!' requires Bool operand: {}", e),
span,
});
}
Type::Bool
}
}
}
fn infer_call(&mut self, func: &Expr, args: &[Expr], span: Span) -> Type {
let func_type = self.infer_expr(func);
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
let result_type = Type::var();
// Include current effects in the expected function type
// This allows calling functions that require effects when those effects are available
let expected_fn = Type::function_with_effects(
arg_types.clone(),
result_type.clone(),
self.current_effects.clone(),
);
match unify(&func_type, &expected_fn) {
Ok(subst) => result_type.apply(&subst),
Err(e) => {
self.errors.push(TypeError {
message: format!("Type mismatch in function call: {}", e),
span,
});
Type::Error
}
}
}
fn infer_effect_op(
&mut self,
effect: &Ident,
operation: &Ident,
args: &[Expr],
span: Span,
) -> Type {
// Check if this is actually a module access, not an effect operation
// This includes stdlib modules (List, String, etc.) and user-imported modules
if let Some(scheme) = self.env.lookup(&effect.name) {
let module_type = scheme.instantiate();
// Get the field type - if it's a Record, treat it as a module
if let Type::Record(fields) = &module_type {
if let Some((_, field_type)) = fields.iter().find(|(n, _)| n == &operation.name) {
// It's a function call on a module field
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
let result_type = Type::var();
let expected_fn = Type::function(arg_types, result_type.clone());
if let Err(e) = unify(field_type, &expected_fn) {
self.errors.push(TypeError {
message: format!(
"Type mismatch in {}.{} call: {}",
effect.name, operation.name, e
),
span,
});
}
return result_type;
} else {
self.errors.push(TypeError {
message: format!(
"Module '{}' has no member '{}'",
effect.name, operation.name
),
span,
});
return Type::Error;
}
}
// Fall through to normal error handling if module field not found
}
// Built-in effects are always available
let builtin_effects = ["Console", "Fail", "State"];
let is_builtin = builtin_effects.contains(&effect.name.as_str());
// Track this effect for inference
if self.inferring_effects {
self.inferred_effects.insert(effect.name.clone());
}
// Check that we're in a context that allows this effect
// Skip this check if we're inferring effects (no explicit declaration)
if !self.inferring_effects && !is_builtin && !self.current_effects.contains(&effect.name) {
self.errors.push(TypeError {
message: format!(
"Effect '{}' not available in current context. Available: {{{}}}",
effect.name, self.current_effects
),
span,
});
}
// Look up the operation - clone to avoid borrow issues
let op = self
.env
.lookup_effect_op(&effect.name, &operation.name)
.cloned();
match op {
Some(op) => {
// Check argument types
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
if arg_types.len() != op.params.len() {
self.errors.push(TypeError {
message: format!(
"Effect operation '{}.{}' expects {} arguments, got {}",
effect.name,
operation.name,
op.params.len(),
arg_types.len()
),
span,
});
}
for (i, (arg_type, (_, param_type))) in
arg_types.iter().zip(op.params.iter()).enumerate()
{
if let Err(e) = unify(arg_type, param_type) {
self.errors.push(TypeError {
message: format!(
"Argument {} of '{}.{}' has type {}, expected {}: {}",
i + 1,
effect.name,
operation.name,
arg_type,
param_type,
e
),
span,
});
}
}
op.return_type.clone()
}
None => {
self.errors.push(TypeError {
message: format!(
"Unknown effect operation: {}.{}",
effect.name, operation.name
),
span,
});
Type::Error
}
}
}
fn infer_field(&mut self, object: &Expr, field: &Ident, span: Span) -> Type {
let object_type = self.infer_expr(object);
match &object_type {
Type::Record(fields) => match fields.iter().find(|(n, _)| n == &field.name) {
Some((_, t)) => t.clone(),
None => {
self.errors.push(TypeError {
message: format!("Record has no field '{}'", field.name),
span,
});
Type::Error
}
},
Type::Var(_) => {
// Can't infer field access on unknown type
Type::var()
}
_ => {
self.errors.push(TypeError {
message: format!(
"Cannot access field '{}' on non-record type {}",
field.name, object_type
),
span,
});
Type::Error
}
}
}
fn infer_lambda(
&mut self,
params: &[Parameter],
return_type: Option<&TypeExpr>,
effects: &[Ident],
body: &Expr,
_span: Span,
) -> Type {
// Set up environment with parameters
let mut local_env = self.env.clone();
let param_types: Vec<Type> = params
.iter()
.map(|p| {
let t = self.resolve_type(&p.typ);
local_env.bind(&p.name.name, TypeScheme::mono(t.clone()));
t
})
.collect();
// Determine if we need to infer effects for this lambda
let explicit_effects = !effects.is_empty();
let declared_effects = EffectSet::from_iter(effects.iter().map(|e| e.name.clone()));
// Save old state
let old_effects = std::mem::replace(&mut self.current_effects, declared_effects.clone());
let old_inferring = std::mem::replace(&mut self.inferring_effects, !explicit_effects);
let old_inferred = std::mem::replace(&mut self.inferred_effects, EffectSet::empty());
// Type check body
let old_env = std::mem::replace(&mut self.env, local_env);
let body_type = self.infer_expr(body);
self.env = old_env;
// Get the inferred effects before restoring state
let inferred = std::mem::replace(&mut self.inferred_effects, old_inferred);
// Restore state
self.current_effects = old_effects;
self.inferring_effects = old_inferring;
// Check return type if specified
let ret_type = if let Some(rt) = return_type {
let declared = self.resolve_type(rt);
if let Err(e) = unify(&body_type, &declared) {
self.errors.push(TypeError {
message: format!(
"Lambda body type {} doesn't match declared {}: {}",
body_type, declared, e
),
span: body.span(),
});
}
declared
} else {
body_type
};
// Use inferred effects if not explicitly declared
let final_effects = if explicit_effects {
declared_effects
} else {
inferred
};
Type::function_with_effects(param_types, ret_type, final_effects)
}
fn infer_let(
&mut self,
name: &Ident,
typ: Option<&TypeExpr>,
value: &Expr,
body: &Expr,
_span: Span,
) -> Type {
let value_type = self.infer_expr(value);
// Check declared type if present
if let Some(type_expr) = typ {
let declared = self.resolve_type(type_expr);
if let Err(e) = unify(&value_type, &declared) {
self.errors.push(TypeError {
message: format!(
"Variable '{}' has type {}, but declared type is {}: {}",
name.name, value_type, declared, e
),
span: name.span,
});
}
}
// Extend environment and check body
let scheme = self.env.generalize(&value_type);
let old_env = self.env.clone();
self.env.bind(&name.name, scheme);
let body_type = self.infer_expr(body);
self.env = old_env;
body_type
}
fn infer_if(
&mut self,
condition: &Expr,
then_branch: &Expr,
else_branch: &Expr,
span: Span,
) -> Type {
let cond_type = self.infer_expr(condition);
if let Err(e) = unify(&cond_type, &Type::Bool) {
self.errors.push(TypeError {
message: format!("If condition must be Bool, got {}: {}", cond_type, e),
span: condition.span(),
});
}
let then_type = self.infer_expr(then_branch);
let else_type = self.infer_expr(else_branch);
match unify(&then_type, &else_type) {
Ok(subst) => then_type.apply(&subst),
Err(e) => {
self.errors.push(TypeError {
message: format!(
"If branches have incompatible types: then={}, else={}: {}",
then_type, else_type, e
),
span,
});
Type::Error
}
}
}
fn infer_match(&mut self, scrutinee: &Expr, arms: &[MatchArm], span: Span) -> Type {
let scrutinee_type = self.infer_expr(scrutinee);
if arms.is_empty() {
self.errors.push(TypeError {
message: "Match expression must have at least one arm".to_string(),
span,
});
return Type::Error;
}
let mut result_type: Option<Type> = None;
for arm in arms {
// Check pattern and get bindings
let bindings = self.check_pattern(&arm.pattern, &scrutinee_type);
// Extend environment with pattern bindings
let old_env = self.env.clone();
for (name, typ) in bindings {
self.env.bind(name, TypeScheme::mono(typ));
}
// Check guard if present
if let Some(ref guard) = arm.guard {
let guard_type = self.infer_expr(guard);
if let Err(e) = unify(&guard_type, &Type::Bool) {
self.errors.push(TypeError {
message: format!("Match guard must be Bool: {}", e),
span: guard.span(),
});
}
}
// Check body
let body_type = self.infer_expr(&arm.body);
self.env = old_env;
// Unify with previous arms
match &result_type {
None => result_type = Some(body_type),
Some(prev) => {
if let Err(e) = unify(prev, &body_type) {
self.errors.push(TypeError {
message: format!(
"Match arm has incompatible type: expected {}, got {}: {}",
prev, body_type, e
),
span: arm.span,
});
}
}
}
}
// Check exhaustiveness
let exhaustiveness = check_exhaustiveness(&scrutinee_type, arms, &self.env);
if !exhaustiveness.is_exhaustive {
let hint = missing_patterns_hint(&exhaustiveness.missing_patterns);
self.errors.push(TypeError {
message: format!(
"Non-exhaustive pattern match. {}",
hint
),
span,
});
}
// Warn about redundant arms
for idx in exhaustiveness.redundant_arms {
self.errors.push(TypeError {
message: format!(
"Redundant pattern: this arm will never be matched because previous patterns cover all cases"
),
span: arms[idx].span,
});
}
result_type.unwrap_or(Type::Error)
}
fn check_pattern(&mut self, pattern: &Pattern, expected: &Type) -> Vec<(String, Type)> {
match pattern {
Pattern::Wildcard(_) => Vec::new(),
Pattern::Var(ident) => {
vec![(ident.name.clone(), expected.clone())]
}
Pattern::Literal(lit) => {
let lit_type = self.infer_literal(lit);
if let Err(e) = unify(&lit_type, expected) {
self.errors.push(TypeError {
message: format!("Pattern literal type mismatch: {}", e),
span: lit.span,
});
}
Vec::new()
}
Pattern::Constructor { name, fields, span } => {
// Look up constructor
// For now, handle Option specially
match name.name.as_str() {
"None" => {
if let Err(e) = unify(expected, &Type::Option(Box::new(Type::var()))) {
self.errors.push(TypeError {
message: format!(
"None pattern doesn't match type {}: {}",
expected, e
),
span: *span,
});
}
Vec::new()
}
"Some" => {
let inner_type = Type::var();
if let Err(e) = unify(expected, &Type::Option(Box::new(inner_type.clone())))
{
self.errors.push(TypeError {
message: format!(
"Some pattern doesn't match type {}: {}",
expected, e
),
span: *span,
});
}
if fields.len() == 1 {
self.check_pattern(&fields[0], &inner_type)
} else {
Vec::new()
}
}
_ => {
// Generic constructor handling
let mut bindings = Vec::new();
for field in fields {
bindings.extend(self.check_pattern(field, &Type::var()));
}
bindings
}
}
}
Pattern::Tuple { elements, span } => {
let element_types: Vec<Type> = elements.iter().map(|_| Type::var()).collect();
if let Err(e) = unify(expected, &Type::Tuple(element_types.clone())) {
self.errors.push(TypeError {
message: format!("Tuple pattern doesn't match type {}: {}", expected, e),
span: *span,
});
}
let mut bindings = Vec::new();
for (elem, typ) in elements.iter().zip(element_types.iter()) {
bindings.extend(self.check_pattern(elem, typ));
}
bindings
}
Pattern::Record { fields, span } => {
let field_types: Vec<(String, Type)> = fields
.iter()
.map(|(n, _)| (n.name.clone(), Type::var()))
.collect();
if let Err(e) = unify(expected, &Type::Record(field_types.clone())) {
self.errors.push(TypeError {
message: format!("Record pattern doesn't match type {}: {}", expected, e),
span: *span,
});
}
let mut bindings = Vec::new();
for ((_, pattern), (_, typ)) in fields.iter().zip(field_types.iter()) {
bindings.extend(self.check_pattern(pattern, typ));
}
bindings
}
}
}
fn infer_block(&mut self, statements: &[Statement], result: &Expr, _span: Span) -> Type {
// Process statements
for stmt in statements {
match stmt {
Statement::Expr(e) => {
self.infer_expr(e);
}
Statement::Let {
name, typ, value, ..
} => {
let value_type = self.infer_expr(value);
if let Some(type_expr) = typ {
let declared = self.resolve_type(type_expr);
if let Err(e) = unify(&value_type, &declared) {
self.errors.push(TypeError {
message: format!(
"Variable '{}' has type {}, but declared type is {}: {}",
name.name, value_type, declared, e
),
span: name.span,
});
}
}
let scheme = self.env.generalize(&value_type);
self.env.bind(&name.name, scheme);
}
}
}
// Return the type of the result expression
self.infer_expr(result)
}
fn infer_record(&mut self, fields: &[(Ident, Expr)], _span: Span) -> Type {
let field_types: Vec<(String, Type)> = fields
.iter()
.map(|(name, expr)| (name.name.clone(), self.infer_expr(expr)))
.collect();
Type::Record(field_types)
}
fn infer_tuple(&mut self, elements: &[Expr], _span: Span) -> Type {
let element_types: Vec<Type> = elements.iter().map(|e| self.infer_expr(e)).collect();
Type::Tuple(element_types)
}
fn infer_list(&mut self, elements: &[Expr], span: Span) -> Type {
if elements.is_empty() {
return Type::List(Box::new(Type::var()));
}
let first_type = self.infer_expr(&elements[0]);
for elem in &elements[1..] {
let elem_type = self.infer_expr(elem);
if let Err(e) = unify(&first_type, &elem_type) {
self.errors.push(TypeError {
message: format!("List elements must have same type: {}", e),
span,
});
}
}
Type::List(Box::new(first_type))
}
fn infer_run(&mut self, expr: &Expr, handlers: &[(Ident, Expr)], _span: Span) -> Type {
// The handlers provide implementations for effects
// After running, those effects are no longer present
// Add the handled effects to available effects
let handled_effects: EffectSet =
EffectSet::from_iter(handlers.iter().map(|(e, _)| e.name.clone()));
// Built-in effects are always available in run blocks (they have runtime implementations)
let builtin_effects: EffectSet =
EffectSet::from_iter(["Console", "Fail", "State"].iter().map(|s| s.to_string()));
// Extend current effects with handled ones and built-in effects
let combined = self.current_effects.union(&handled_effects).union(&builtin_effects);
let old_effects = std::mem::replace(&mut self.current_effects, combined);
// Type check the expression
let result_type = self.infer_expr(expr);
// Type check handlers
for (effect_name, handler_expr) in handlers {
// Just check the handler expression for now
let _ = self.infer_expr(handler_expr);
// Verify the effect exists
if self.env.lookup_effect(&effect_name.name).is_none() {
// Find similar effect names for suggestion
let available_effects: Vec<&str> = self.env.effects.keys()
.map(|s| s.as_str())
.collect();
let suggestions = find_similar_names(&effect_name.name, available_effects, 2);
let mut message = format!("Unknown effect: {}", effect_name.name);
if let Some(hint) = format_did_you_mean(&suggestions) {
message.push_str(&format!(". {}", hint));
}
self.errors.push(TypeError {
message,
span: effect_name.span,
});
}
}
self.current_effects = old_effects;
result_type
}
// Helper methods
fn function_type(&mut self, func: &FunctionDecl) -> TypeScheme {
// Save old type params and start fresh for this function's scope
let old_params = std::mem::take(&mut self.type_params);
// Bind type parameters to fresh type variables
let mut bound_vars = Vec::new();
for param in &func.type_params {
let var = Type::var();
if let Type::Var(n) = &var {
bound_vars.push(*n);
}
self.type_params.insert(param.name.clone(), var);
}
// Resolve parameter and return types (will use type_params for generics)
let param_types: Vec<Type> = func
.params
.iter()
.map(|p| self.resolve_type(&p.typ))
.collect();
let return_type = self.resolve_type(&func.return_type);
let effects = EffectSet::from_iter(func.effects.iter().map(|e| e.name.clone()));
let properties = PropertySet::from_ast(&func.properties);
// Restore old type params
self.type_params = old_params;
// Return polymorphic type scheme with bound variables
TypeScheme {
vars: bound_vars,
typ: Type::function_with_properties(param_types, return_type, effects, properties),
}
}
fn effect_def(&self, effect: &EffectDecl) -> EffectDef {
EffectDef {
name: effect.name.name.clone(),
type_params: effect.type_params.iter().map(|p| p.name.clone()).collect(),
operations: effect
.operations
.iter()
.map(|op| EffectOpDef {
name: op.name.name.clone(),
params: op
.params
.iter()
.map(|p| (p.name.name.clone(), self.resolve_type(&p.typ)))
.collect(),
return_type: self.resolve_type(&op.return_type),
})
.collect(),
}
}
fn type_def(&self, type_decl: &TypeDecl) -> types::TypeDef {
match &type_decl.definition {
ast::TypeDef::Alias(t) => types::TypeDef::Alias(self.resolve_type(t)),
ast::TypeDef::Record(fields) => types::TypeDef::Record(
fields
.iter()
.map(|f| (f.name.name.clone(), self.resolve_type(&f.typ)))
.collect(),
),
ast::TypeDef::Enum(variants) => types::TypeDef::Enum(
variants
.iter()
.map(|v| VariantDef {
name: v.name.name.clone(),
fields: match &v.fields {
VariantFields::Unit => VariantFieldsDef::Unit,
VariantFields::Tuple(types) => VariantFieldsDef::Tuple(
types.iter().map(|t| self.resolve_type(t)).collect(),
),
VariantFields::Record(fields) => VariantFieldsDef::Record(
fields
.iter()
.map(|f| (f.name.name.clone(), self.resolve_type(&f.typ)))
.collect(),
),
},
})
.collect(),
),
}
}
fn handler_def(&self, handler: &HandlerDecl) -> HandlerDef {
HandlerDef {
name: handler.name.name.clone(),
effect: handler.effect.name.clone(),
params: handler
.params
.iter()
.map(|p| (p.name.name.clone(), self.resolve_type(&p.typ)))
.collect(),
}
}
fn trait_def(&self, trait_decl: &TraitDecl) -> TraitDef {
let methods = trait_decl
.methods
.iter()
.map(|m| TraitMethodDef {
name: m.name.name.clone(),
type_params: m.type_params.iter().map(|p| p.name.clone()).collect(),
params: m
.params
.iter()
.map(|p| (p.name.name.clone(), self.resolve_type(&p.typ)))
.collect(),
return_type: self.resolve_type(&m.return_type),
has_default: m.default_impl.is_some(),
})
.collect();
let super_traits = trait_decl
.super_traits
.iter()
.map(|b| TraitBoundDef {
trait_name: b.trait_name.name.clone(),
type_args: b.type_args.iter().map(|t| self.resolve_type(t)).collect(),
})
.collect();
TraitDef {
name: trait_decl.name.name.clone(),
type_params: trait_decl.type_params.iter().map(|p| p.name.clone()).collect(),
super_traits,
methods,
}
}
fn collect_impl(&self, impl_decl: &ImplDecl) -> TraitImpl {
use std::collections::HashMap;
let methods: HashMap<String, Type> = impl_decl
.methods
.iter()
.map(|m| {
let return_type = m
.return_type
.as_ref()
.map(|t| self.resolve_type(t))
.unwrap_or_else(Type::var);
let param_types: Vec<Type> = m
.params
.iter()
.map(|p| self.resolve_type(&p.typ))
.collect();
let func_type = Type::function(param_types, return_type);
(m.name.name.clone(), func_type)
})
.collect();
let constraints = impl_decl
.constraints
.iter()
.map(|c| {
let bounds = c
.bounds
.iter()
.map(|b| TraitBoundDef {
trait_name: b.trait_name.name.clone(),
type_args: b.type_args.iter().map(|t| self.resolve_type(t)).collect(),
})
.collect();
(c.type_param.name.clone(), bounds)
})
.collect();
TraitImpl {
trait_name: impl_decl.trait_name.name.clone(),
trait_args: impl_decl
.trait_args
.iter()
.map(|t| self.resolve_type(t))
.collect(),
target_type: self.resolve_type(&impl_decl.target_type),
type_params: impl_decl.type_params.iter().map(|p| p.name.clone()).collect(),
constraints,
methods,
}
}
fn check_impl(&mut self, impl_decl: &ImplDecl) {
// Verify the trait exists
let trait_name = &impl_decl.trait_name.name;
let trait_def = match self.env.traits.get(trait_name) {
Some(def) => def.clone(),
None => {
self.errors.push(TypeError {
message: format!("Unknown trait: {}", trait_name),
span: impl_decl.span,
});
return;
}
};
// Verify all required methods are implemented
for method_def in &trait_def.methods {
if !method_def.has_default {
let implemented = impl_decl.methods.iter().any(|m| m.name.name == method_def.name);
if !implemented {
self.errors.push(TypeError {
message: format!(
"Missing implementation for required method '{}' of trait '{}'",
method_def.name, trait_name
),
span: impl_decl.span,
});
}
}
}
// Type check each implemented method
for impl_method in &impl_decl.methods {
// Find the method signature in the trait
let method_def = trait_def.methods.iter().find(|m| m.name == impl_method.name.name);
if method_def.is_none() {
self.errors.push(TypeError {
message: format!(
"Method '{}' is not defined in trait '{}'",
impl_method.name.name, trait_name
),
span: impl_method.span,
});
continue;
}
// Set up local environment with parameters
let mut local_env = self.env.clone();
for param in &impl_method.params {
let param_type = self.resolve_type(&param.typ);
local_env.bind(&param.name.name, TypeScheme::mono(param_type));
}
// Type check the body
let old_env = std::mem::replace(&mut self.env, local_env);
let body_type = self.infer_expr(&impl_method.body);
self.env = old_env;
// Check return type matches if specified
if let Some(ref return_type_expr) = impl_method.return_type {
let return_type = self.resolve_type(return_type_expr);
if let Err(e) = unify(&body_type, &return_type) {
self.errors.push(TypeError {
message: format!(
"Method '{}' body has type {}, but declared return type is {}: {}",
impl_method.name.name, body_type, return_type, e
),
span: impl_method.span,
});
}
}
}
}
fn resolve_type(&self, type_expr: &TypeExpr) -> Type {
match type_expr {
TypeExpr::Named(ident) => match ident.name.as_str() {
"Int" => Type::Int,
"Float" => Type::Float,
"Bool" => Type::Bool,
"String" => Type::String,
"Char" => Type::Char,
"Unit" => Type::Unit,
"_" => Type::var(),
name => {
// Check if it's a type parameter in scope (for generics)
if let Some(var) = self.type_params.get(name) {
return var.clone();
}
Type::Named(name.to_string())
}
},
TypeExpr::App(constructor, args) => {
let resolved_args: Vec<Type> = args.iter().map(|a| self.resolve_type(a)).collect();
// Handle built-in generic types
if let TypeExpr::Named(name) = constructor.as_ref() {
match name.name.as_str() {
"List" if resolved_args.len() == 1 => {
return Type::List(Box::new(resolved_args[0].clone()));
}
"Option" if resolved_args.len() == 1 => {
return Type::Option(Box::new(resolved_args[0].clone()));
}
_ => {}
}
}
Type::App {
constructor: Box::new(self.resolve_type(constructor)),
args: resolved_args,
}
}
TypeExpr::Function {
params,
return_type,
effects,
} => Type::function_with_effects(
params.iter().map(|p| self.resolve_type(p)).collect(),
self.resolve_type(return_type),
EffectSet::from_iter(effects.iter().map(|e| e.name.clone())),
),
TypeExpr::Tuple(elements) => {
Type::Tuple(elements.iter().map(|e| self.resolve_type(e)).collect())
}
TypeExpr::Record(fields) => Type::Record(
fields
.iter()
.map(|f| (f.name.name.clone(), self.resolve_type(&f.typ)))
.collect(),
),
TypeExpr::Unit => Type::Unit,
TypeExpr::Versioned {
base,
constraint: _,
} => {
// For now, resolve the base type and ignore versioning
// Full version tracking will be added in the type system
self.resolve_type(base)
}
}
}
}
impl Default for TypeChecker {
fn default() -> Self {
Self::new()
}
}