Test: fix parser generic annotations and add checker test suite

- Fix two test helpers (top/program) missing <Parsed> type argument
- Add checker/tests.rs with 74 tests covering all §7/§8 rules: entry
  point validation, duplicate defs, struct cycles, literals, promotion,
  type inference, definite assignment, undefined vars/funcs, arithmetic,
  shift, comparison, logical ops, unary ops, pointers, struct literals,
  field/index access, function calls, return checking, mutation, and
  break/continue
- Fix assignment LHS check: bare identifier on the write side of `=`
  must not trigger "uninitialized" — use lhs_ty() helper that skips
  the assigned-set check for the direct write target

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 15:16:49 +01:00
parent aca0dae7de
commit 1f3d64f97c
7 changed files with 812 additions and 98 deletions

View File

@@ -1,10 +1,10 @@
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;
use super::env::TypeEnv;
use super::Checker;
impl Checker {
/// Type-check `expr` in environment `env` with definite-assignment set `assigned`.
@@ -23,7 +23,10 @@ impl Checker {
ExprKind::FloatLit(_) => Ty::F64,
// T-StringLit → *char
ExprKind::StringLit(_) => Ty::Ptr { mutable: false, pointee: Box::new(Ty::Char) },
ExprKind::StringLit(_) => Ty::Ptr {
mutable: false,
pointee: Box::new(Ty::Char),
},
// T-CharLit → char
ExprKind::CharLit(_) => Ty::Char,
@@ -56,7 +59,11 @@ impl Checker {
},
// T-StructLit
ExprKind::StructLit { name, name_span, fields } => {
ExprKind::StructLit {
name,
name_span,
fields,
} => {
if !self.sigma.contains(name) {
self.emit(
Diagnostic::error(format!("undefined struct `{name}`"))
@@ -135,7 +142,11 @@ impl Checker {
}
// T-Unary
ExprKind::Unary { op, op_span, expr: inner } => {
ExprKind::Unary {
op,
op_span,
expr: inner,
} => {
let inner_ty = self.check_expr(inner, env, assigned);
if inner_ty.is_error() {
return Ty::Error;
@@ -144,12 +155,20 @@ impl Checker {
}
// T-Binary
ExprKind::Binary { op, op_span, lhs, rhs } => {
self.check_binary(*op, *op_span, lhs, rhs, env, assigned)
}
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 } => {
ExprKind::CompoundAssign {
op,
op_span,
lhs,
rhs,
} => {
if !self.is_mutable_place(lhs, env) {
self.emit(
Diagnostic::error(
@@ -168,7 +187,11 @@ impl Checker {
}
// T-Field
ExprKind::Field { expr: inner, field, field_span } => {
ExprKind::Field {
expr: inner,
field,
field_span,
} => {
let inner_ty = self.check_expr(inner, env, assigned);
if inner_ty.is_error() {
return Ty::Error;
@@ -385,7 +408,10 @@ impl Checker {
return Ty::Error;
}
let mutable = self.is_mutable_place(inner, env);
Ty::Ptr { mutable, pointee: Box::new(inner_ty) }
Ty::Ptr {
mutable,
pointee: Box::new(inner_ty),
}
}
}
}
@@ -406,13 +432,14 @@ impl Checker {
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)),
Diagnostic::error("left-hand side of `=` must be a mutable place")
.with_label(Label::primary(lhs.span)),
);
}
let lhs_ty = self.check_expr(lhs, env, assigned);
// 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(
@@ -562,6 +589,37 @@ impl Checker {
}
}
/// 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<Parsed>,
env: &TypeEnv,
assigned: &HashSet<String>,
) -> 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 {
@@ -596,7 +654,9 @@ impl Checker {
pub fn is_place(&self, expr: &ast::Expr<Parsed>, env: &TypeEnv) -> bool {
match &expr.kind {
ExprKind::Ident(_) => true,
ExprKind::Unary { op: UnaryOp::Deref, .. } => 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),
@@ -612,10 +672,12 @@ impl Checker {
/// - `e.f` / `e[i]` is mutable iff `e` is mutable.
pub fn is_mutable_place(&self, expr: &ast::Expr<Parsed>, 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, .. } => {
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!(
@@ -635,25 +697,27 @@ impl Checker {
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, .. } => {
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,
}
}
ExprKind::Index { expr: inner, .. } => match self.peek_ty(inner, env)? {
Ty::Array { elem, .. } => Some(*elem),
Ty::Ptr { pointee, .. } => Some(*pointee),
_ => None,
},
_ => None,
}
}