use std::collections::HashSet; use super::Checker; use super::env::TypeEnv; use crate::ast::{self, BinaryOp, CompoundAssignOp, ExprKind, Parsed, Ty, UnaryOp}; use crate::diagnostics::{Diagnostic, Label}; use crate::token::Span; impl Checker { /// Type-check `expr` in environment `env` with definite-assignment set `assigned`. /// Returns the resolved type. Does NOT mutate `env` or `assigned`. pub fn check_expr( &mut self, expr: &ast::Expr, env: &TypeEnv, assigned: &HashSet, ) -> Ty { match &expr.kind { // T-IntLit → UnboundInt (resolved from context; defaults to i32) ExprKind::IntLit(_) => Ty::UnboundInt, // T-FloatLit → f64 ExprKind::FloatLit(_) => Ty::F64, // T-StringLit → *char ExprKind::StringLit(_) => Ty::Ptr { mutable: false, pointee: Box::new(Ty::Char), }, // T-CharLit → char ExprKind::CharLit(_) => Ty::Char, // T-Bool → bool ExprKind::Bool(_) => Ty::Bool, // T-Ident ExprKind::Ident(name) => match env.lookup(name) { Some((ty, _)) => { if !assigned.contains(name) { self.emit( Diagnostic::error(format!( "use of potentially uninitialized variable `{name}`" )) .with_label(Label::primary(expr.span)), ); Ty::Error } else { ty.clone() } } None => { self.emit( Diagnostic::error(format!("undefined variable `{name}`")) .with_label(Label::primary(expr.span)), ); Ty::Error } }, // T-StructLit ExprKind::StructLit { name, name_span, fields, } => { if !self.sigma.contains(name) { self.emit( Diagnostic::error(format!("undefined struct `{name}`")) .with_label(Label::primary(*name_span)), ); // Still check field expressions for further errors for sf in fields { self.check_expr(&sf.value, env, assigned); } return Ty::Error; } let expected: Vec<(String, Ty)> = self .sigma .fields(name) .unwrap_or(&[]) .iter() .map(|f| (f.name.clone(), f.ty.clone())) .collect(); let mut provided: HashSet = HashSet::new(); for sf in fields { let val_ty = self.check_expr(&sf.value, env, assigned); if provided.contains(&sf.name) { self.emit( Diagnostic::error(format!( "field `{}` specified more than once", sf.name )) .with_label(Label::primary(sf.name_span)), ); continue; } provided.insert(sf.name.clone()); match expected.iter().find(|(n, _)| *n == sf.name) { Some((_, exp_ty)) => { if !val_ty.is_error() && !exp_ty.is_error() && !val_ty.promotes_to(exp_ty) { self.emit( Diagnostic::error(format!( "field `{}`: expected `{}`, got `{}`", sf.name, exp_ty.display(), val_ty.display() )) .with_label(Label::primary(sf.value.span)), ); } } None => { self.emit( Diagnostic::error(format!( "struct `{name}` has no field `{}`", sf.name )) .with_label(Label::primary(sf.name_span)), ); } } } for (fname, _) in &expected { if !provided.contains(fname) { self.emit( Diagnostic::error(format!( "missing field `{fname}` in struct literal `{name}`" )) .with_label(Label::primary(*name_span)), ); } } Ty::Struct(name.clone()) } // T-Unary ExprKind::Unary { op, op_span, expr: inner, } => { let inner_ty = self.check_expr(inner, env, assigned); if inner_ty.is_error() { return Ty::Error; } self.check_unary(*op, *op_span, inner, inner_ty, env) } // T-Binary ExprKind::Binary { op, op_span, lhs, rhs, } => self.check_binary(*op, *op_span, lhs, rhs, env, assigned), // T-CompoundAssign: lhs op= rhs (expands to lhs = lhs op rhs) ExprKind::CompoundAssign { op, op_span, lhs, rhs, } => { if !self.is_mutable_place(lhs, env) { self.emit( Diagnostic::error( "left-hand side of compound assignment must be a mutable place", ) .with_label(Label::primary(lhs.span)), ); } let lhs_ty = self.check_expr(lhs, env, assigned); let rhs_ty = self.check_expr(rhs, env, assigned); if !lhs_ty.is_error() && !rhs_ty.is_error() { let bin_op = compound_to_binary(*op); self.check_arith_or_shift(lhs_ty, rhs_ty, bin_op, *op_span); } Ty::Unit } // T-Field ExprKind::Field { expr: inner, field, field_span, } => { let inner_ty = self.check_expr(inner, env, assigned); if inner_ty.is_error() { return Ty::Error; } let struct_name = match &inner_ty { Ty::Struct(n) => n.clone(), _ => { self.emit( Diagnostic::error(format!( "field access on non-struct type `{}`", inner_ty.display() )) .with_label(Label::primary(*field_span)), ); return Ty::Error; } }; match self.sigma.field_ty(&struct_name, field) { Some(ty) => ty.clone(), None => { self.emit( Diagnostic::error(format!( "struct `{struct_name}` has no field `{field}`" )) .with_label(Label::primary(*field_span)), ); Ty::Error } } } // T-Index ExprKind::Index { expr: inner, index } => { let inner_ty = self.check_expr(inner, env, assigned); let idx_ty = self.check_expr(index, env, assigned); if inner_ty.is_error() { return Ty::Error; } if !idx_ty.is_error() && !idx_ty.is_integer() { self.emit( Diagnostic::error(format!( "index must be an integer type, got `{}`", idx_ty.display() )) .with_label(Label::primary(index.span)), ); } match inner_ty { Ty::Array { elem, .. } => *elem, Ty::Ptr { pointee, .. } => *pointee, _ => { self.emit( Diagnostic::error(format!( "indexing requires array or pointer, got `{}`", inner_ty.display() )) .with_label(Label::primary(inner.span)), ); Ty::Error } } } // T-Call ExprKind::Call { callee, args } => { let func_name = match &callee.kind { ExprKind::Ident(name) => name.clone(), _ => { self.emit( Diagnostic::error("callee must be a function name") .with_label(Label::primary(callee.span)), ); for arg in args { self.check_expr(arg, env, assigned); } return Ty::Error; } }; let (param_tys, ret_ty) = match self.phi.get(&func_name) { Some(entry) => { let pts: Vec = entry.params.iter().map(|p| p.ty.clone()).collect(); let ret = entry.ret.clone(); (pts, ret) } None => { self.emit( Diagnostic::error(format!("undefined function `{func_name}`")) .with_label(Label::primary(callee.span)), ); for arg in args { self.check_expr(arg, env, assigned); } return Ty::Error; } }; if args.len() != param_tys.len() { self.emit( Diagnostic::error(format!( "`{func_name}` expects {} argument(s), got {}", param_tys.len(), args.len() )) .with_label(Label::primary(callee.span)), ); } for (i, arg) in args.iter().enumerate() { let arg_ty = self.check_expr(arg, env, assigned); if let Some(exp) = param_tys.get(i) { if !arg_ty.is_error() && !exp.is_error() && !arg_ty.promotes_to(exp) { self.emit( Diagnostic::error(format!( "argument {}: expected `{}`, got `{}`", i + 1, exp.display(), arg_ty.display() )) .with_label(Label::primary(arg.span)), ); } } } ret_ty } ExprKind::Group(inner) => self.check_expr(inner, env, assigned), ExprKind::Error => Ty::Error, } } // ── Unary helper ────────────────────────────────────────────────────────── fn check_unary( &mut self, op: UnaryOp, op_span: Span, inner: &ast::Expr, inner_ty: Ty, env: &TypeEnv, ) -> Ty { match op { UnaryOp::Neg => { // UnboundInt is a literal-origin integer; negating keeps it unbound. if matches!(inner_ty, Ty::UnboundInt) { return Ty::UnboundInt; } if !inner_ty.is_signed() && !inner_ty.is_float() { self.emit( Diagnostic::error(format!( "unary `-` requires a signed integer or float, got `{}`", inner_ty.display() )) .with_label(Label::primary(op_span)), ); Ty::Error } else { inner_ty } } UnaryOp::Not => { if inner_ty != Ty::Bool { self.emit( Diagnostic::error(format!( "unary `!` requires `bool`, got `{}`", inner_ty.display() )) .with_label(Label::primary(op_span)), ); Ty::Error } else { Ty::Bool } } UnaryOp::BitNot => { if !inner_ty.is_integer() { self.emit( Diagnostic::error(format!( "unary `~` requires an integer type, got `{}`", inner_ty.display() )) .with_label(Label::primary(op_span)), ); Ty::Error } else { inner_ty } } UnaryOp::Deref => match &inner_ty { Ty::Ptr { pointee, .. } => *pointee.clone(), Ty::OpaquePtr { .. } => { self.emit( Diagnostic::error("cannot dereference an opaque pointer") .with_label(Label::primary(op_span)), ); Ty::Error } _ => { self.emit( Diagnostic::error(format!( "unary `*` requires a pointer, got `{}`", inner_ty.display() )) .with_label(Label::primary(op_span)), ); Ty::Error } }, UnaryOp::AddrOf => { if !self.is_place(inner, env) { self.emit( Diagnostic::error("cannot take address of a non-place expression") .with_label(Label::primary(op_span)), ); return Ty::Error; } let mutable = self.is_mutable_place(inner, env); Ty::Ptr { mutable, pointee: Box::new(inner_ty), } } } } // ── Binary helper ───────────────────────────────────────────────────────── fn check_binary( &mut self, op: BinaryOp, op_span: Span, lhs: &ast::Expr, rhs: &ast::Expr, env: &TypeEnv, assigned: &HashSet, ) -> Ty { match op { // T-Assign BinaryOp::Assign => { if !self.is_mutable_place(lhs, env) { self.emit( Diagnostic::error("left-hand side of `=` must be a mutable place") .with_label(Label::primary(lhs.span)), ); } // For a bare identifier on the LHS we only need its declared // type, not a read — don't emit "uninitialized" for the target // of the assignment itself. let lhs_ty = self.lhs_ty(lhs, env, assigned); let rhs_ty = self.check_expr(rhs, env, assigned); if !lhs_ty.is_error() && !rhs_ty.is_error() && !rhs_ty.promotes_to(&lhs_ty) { self.emit( Diagnostic::error(format!( "type mismatch: expected `{}`, got `{}`", lhs_ty.display(), rhs_ty.display() )) .with_label(Label::primary(rhs.span)), ); } Ty::Unit } // Logical BinaryOp::Or | BinaryOp::And => { let lhs_ty = self.check_expr(lhs, env, assigned); let rhs_ty = self.check_expr(rhs, env, assigned); let kw = if op == BinaryOp::Or { "or" } else { "and" }; if !lhs_ty.is_error() && lhs_ty != Ty::Bool { self.emit( Diagnostic::error(format!( "`{kw}` requires `bool` operands, left side is `{}`", lhs_ty.display() )) .with_label(Label::primary(lhs.span)), ); } if !rhs_ty.is_error() && rhs_ty != Ty::Bool { self.emit( Diagnostic::error(format!( "`{kw}` requires `bool` operands, right side is `{}`", rhs_ty.display() )) .with_label(Label::primary(rhs.span)), ); } Ty::Bool } // Equality: compatible types OR pointer-compatible BinaryOp::Eq | BinaryOp::Ne => { let lhs_ty = self.check_expr(lhs, env, assigned); let rhs_ty = self.check_expr(rhs, env, assigned); if !lhs_ty.is_error() && !rhs_ty.is_error() { let ok = Ty::common(&lhs_ty, &rhs_ty).is_some() || Ty::ptr_common(&lhs_ty, &rhs_ty).is_some(); if !ok { self.emit( Diagnostic::error(format!( "cannot compare `{}` with `{}`", lhs_ty.display(), rhs_ty.display() )) .with_label(Label::primary(op_span)), ); } } Ty::Bool } // Ordering: numeric or char (UnboundInt resolves to the other operand's type) BinaryOp::Lt | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Ge => { let lhs_ty = self.check_expr(lhs, env, assigned); let rhs_ty = self.check_expr(rhs, env, assigned); if !lhs_ty.is_error() && !rhs_ty.is_error() { let ok = match Ty::common(&lhs_ty, &rhs_ty) { Some(ref c) => c.is_numeric() || *c == Ty::Char, None => false, }; if !ok { self.emit( Diagnostic::error(format!( "cannot order `{}` and `{}`", lhs_ty.display(), rhs_ty.display() )) .with_label(Label::primary(op_span)), ); } } Ty::Bool } // Bitwise: require integer types (UnboundInt is fine) BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::BitAnd => { let lhs_ty = self.check_expr(lhs, env, assigned); let rhs_ty = self.check_expr(rhs, env, assigned); if lhs_ty.is_error() || rhs_ty.is_error() { return Ty::Error; } match Ty::common(&lhs_ty, &rhs_ty) { Some(ref c) if c.is_integer() => c.clone(), _ => { self.emit( Diagnostic::error(format!( "bitwise operator requires integer operands, got `{}` and `{}`", lhs_ty.display(), rhs_ty.display() )) .with_label(Label::primary(op_span)), ); Ty::Error } } } // Shift: LHS integer, RHS any integer; result type = LHS type. // If LHS is UnboundInt and RHS is concrete, result is still UnboundInt // (it will be resolved when the result is used in context). BinaryOp::Shl | BinaryOp::Shr => { let lhs_ty = self.check_expr(lhs, env, assigned); let rhs_ty = self.check_expr(rhs, env, assigned); if lhs_ty.is_error() || rhs_ty.is_error() { return Ty::Error; } if !lhs_ty.is_integer() { self.emit( Diagnostic::error(format!( "shift requires an integer LHS, got `{}`", lhs_ty.display() )) .with_label(Label::primary(lhs.span)), ); return Ty::Error; } if !rhs_ty.is_integer() { self.emit( Diagnostic::error(format!( "shift amount must be an integer, got `{}`", rhs_ty.display() )) .with_label(Label::primary(rhs.span)), ); return Ty::Error; } lhs_ty } // Arithmetic BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Rem => { let lhs_ty = self.check_expr(lhs, env, assigned); let rhs_ty = self.check_expr(rhs, env, assigned); if lhs_ty.is_error() || rhs_ty.is_error() { return Ty::Error; } self.check_arith_or_shift(lhs_ty, rhs_ty, op, op_span) } } } /// Return the declared type of `expr` as a write-side place, without /// checking that it has been initialised first. For a bare identifier /// this avoids a spurious "uninitialized" diagnostic on the target of /// an assignment like `x = 5` when `x` is being given its first value. /// /// For all other place forms (deref, field, index) the sub-expression IS /// read, so the normal `check_expr` path (with assigned checking) is used. fn lhs_ty( &mut self, expr: &ast::Expr, env: &TypeEnv, assigned: &HashSet, ) -> Ty { match &expr.kind { ExprKind::Ident(name) => match env.lookup(name) { Some((ty, _)) => ty.clone(), None => { self.emit( Diagnostic::error(format!("undefined variable `{name}`")) .with_label(Label::primary(expr.span)), ); Ty::Error } }, ExprKind::Group(inner) => self.lhs_ty(inner, env, assigned), // All other place forms (deref, field, index) involve a read of // the sub-expression — use the full check. _ => self.check_expr(expr, env, assigned), } } /// Check that `lhs_ty op rhs_ty` is a valid arithmetic expression; /// returns the result type. fn check_arith_or_shift(&mut self, lhs_ty: Ty, rhs_ty: Ty, op: BinaryOp, op_span: Span) -> Ty { match Ty::common(&lhs_ty, &rhs_ty) { Some(Ty::Error) => Ty::Error, // UnboundInt + UnboundInt → still UnboundInt (resolved downstream) Some(Ty::UnboundInt) => Ty::UnboundInt, Some(ref c) if c.is_numeric() => c.clone(), _ => { let sym = match op { BinaryOp::Add => "+", BinaryOp::Sub => "-", BinaryOp::Mul => "*", BinaryOp::Div => "/", BinaryOp::Rem => "%", _ => "op", }; self.emit( Diagnostic::error(format!( "`{sym}` requires numeric operands, got `{}` and `{}`", lhs_ty.display(), rhs_ty.display() )) .with_label(Label::primary(op_span)), ); Ty::Error } } } // ── Place predicates ────────────────────────────────────────────────────── /// Returns true if `expr` is a syntactic place (lvalue). pub fn is_place(&self, expr: &ast::Expr, env: &TypeEnv) -> bool { match &expr.kind { ExprKind::Ident(_) => true, ExprKind::Unary { op: UnaryOp::Deref, .. } => true, ExprKind::Field { expr: inner, .. } => self.is_place(inner, env), ExprKind::Index { expr: inner, .. } => self.is_place(inner, env), ExprKind::Group(inner) => self.is_place(inner, env), _ => false, } } /// Returns true if `expr` is a mutable place. /// /// Rules (§6.3): /// - `x` is mutable iff `x` is declared `mut`. /// - `*e` is mutable iff `e` has type `*mut T`. /// - `e.f` / `e[i]` is mutable iff `e` is mutable. pub fn is_mutable_place(&self, expr: &ast::Expr, env: &TypeEnv) -> bool { match &expr.kind { ExprKind::Ident(name) => env.lookup(name).map(|(_, m)| m).unwrap_or(false), ExprKind::Unary { op: UnaryOp::Deref, expr: inner, .. } => { // Mutable iff inner's type is *mut T. // We resolve the type of inner from env without emitting errors. matches!( self.peek_ty(inner, env), Some(Ty::Ptr { mutable: true, .. }) ) } ExprKind::Field { expr: inner, .. } => self.is_mutable_place(inner, env), ExprKind::Index { expr: inner, .. } => self.is_mutable_place(inner, env), ExprKind::Group(inner) => self.is_mutable_place(inner, env), _ => false, } } /// Lightweight type inference for place mutability — does NOT emit diagnostics. fn peek_ty(&self, expr: &ast::Expr, env: &TypeEnv) -> Option { match &expr.kind { ExprKind::Ident(name) => env.lookup(name).map(|(ty, _)| ty.clone()), ExprKind::Group(inner) => self.peek_ty(inner, env), ExprKind::Unary { op: UnaryOp::Deref, expr: inner, .. } => match self.peek_ty(inner, env)? { Ty::Ptr { pointee, .. } => Some(*pointee), _ => None, }, ExprKind::Field { expr: inner, field, .. } => { let Ty::Struct(sname) = self.peek_ty(inner, env)? else { return None; }; self.sigma.field_ty(&sname, field).cloned() } ExprKind::Index { expr: inner, .. } => match self.peek_ty(inner, env)? { Ty::Array { elem, .. } => Some(*elem), Ty::Ptr { pointee, .. } => Some(*pointee), _ => None, }, _ => None, } } } // ── Helpers ─────────────────────────────────────────────────────────────────── fn compound_to_binary(op: CompoundAssignOp) -> BinaryOp { match op { CompoundAssignOp::Add => BinaryOp::Add, CompoundAssignOp::Sub => BinaryOp::Sub, CompoundAssignOp::Mul => BinaryOp::Mul, CompoundAssignOp::Div => BinaryOp::Div, CompoundAssignOp::Rem => BinaryOp::Rem, CompoundAssignOp::BitAnd => BinaryOp::BitAnd, CompoundAssignOp::BitOr => BinaryOp::BitOr, CompoundAssignOp::BitXor => BinaryOp::BitXor, CompoundAssignOp::Shl => BinaryOp::Shl, CompoundAssignOp::Shr => BinaryOp::Shr, } }