From aca0dae7de1762deb4662a2d0508bbdfe679443b Mon Sep 17 00:00:00 2001 From: Jooris Hadeler Date: Wed, 11 Mar 2026 15:12:10 +0100 Subject: [PATCH] Feat: add type-state AST and semantic analysis pass - Update ast.rs with Phase trait (Parsed/Typed), Ty enum, and generic AST nodes so the same tree works pre- and post-type-checking - Add checker/ module implementing the 4-pass semantic analyser from SEMANTICS.md: struct/function collection, field resolution + size-cycle detection, full expression/statement type checking, and entry-point validation - Wire checker into main; semantic errors are only run when the parse succeeds and are rendered with the same diagnostic machinery Co-Authored-By: Claude Sonnet 4.6 --- fluxc/src/ast.rs | 277 +++++++++++++--- fluxc/src/checker/env.rs | 158 +++++++++ fluxc/src/checker/expr.rs | 677 ++++++++++++++++++++++++++++++++++++++ fluxc/src/checker/mod.rs | 276 ++++++++++++++++ fluxc/src/checker/stmt.rs | 365 ++++++++++++++++++++ fluxc/src/main.rs | 9 +- 6 files changed, 1715 insertions(+), 47 deletions(-) create mode 100644 fluxc/src/checker/env.rs create mode 100644 fluxc/src/checker/expr.rs create mode 100644 fluxc/src/checker/mod.rs create mode 100644 fluxc/src/checker/stmt.rs diff --git a/fluxc/src/ast.rs b/fluxc/src/ast.rs index e8a342d..c312711 100644 --- a/fluxc/src/ast.rs +++ b/fluxc/src/ast.rs @@ -1,5 +1,187 @@ use crate::token::Span; +// ── Phase type-state ─────────────────────────────────────────────────────────── + +pub trait Phase { + type ExprExtra: std::fmt::Debug + Clone; +} + +#[derive(Debug, Clone)] +pub struct Parsed; + +#[derive(Debug, Clone)] +pub struct Typed; + +impl Phase for Parsed { + type ExprExtra = (); +} + +impl Phase for Typed { + type ExprExtra = Ty; +} + +// ── Resolved type system ─────────────────────────────────────────────────────── + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Ty { + // Unsigned integers + U8, + U16, + U32, + U64, + // Signed integers + I8, + I16, + I32, + I64, + // Floating-point + F32, + F64, + // Other primitives + Bool, + Char, + Unit, + // Pointer types + Ptr { mutable: bool, pointee: Box }, + OpaquePtr { mutable: bool }, + // Array type + Array { elem: Box, size: u64 }, + // User-defined struct + Struct(String), + // Internal function signature (not user-facing) + FnSig { params: Vec, ret: Box }, + // Error propagation sentinel + Error, +} + +impl Ty { + pub fn is_error(&self) -> bool { + matches!(self, Ty::Error) + } + + pub fn is_unsigned(&self) -> bool { + matches!(self, Ty::U8 | Ty::U16 | Ty::U32 | Ty::U64) + } + + pub fn is_signed(&self) -> bool { + matches!(self, Ty::I8 | Ty::I16 | Ty::I32 | Ty::I64) + } + + pub fn is_integer(&self) -> bool { + self.is_unsigned() || self.is_signed() + } + + pub fn is_float(&self) -> bool { + matches!(self, Ty::F32 | Ty::F64) + } + + pub fn is_numeric(&self) -> bool { + self.is_integer() || self.is_float() + } + + /// Rank within a category: U8/I8=1, U16/I16=2, U32/I32=3, U64/I64=4, F32=1, F64=2 + pub fn rank(&self) -> Option { + match self { + Ty::U8 | Ty::I8 => Some(1), + Ty::U16 | Ty::I16 => Some(2), + Ty::U32 | Ty::I32 => Some(3), + Ty::U64 | Ty::I64 => Some(4), + Ty::F32 => Some(1), + Ty::F64 => Some(2), + _ => None, + } + } + + /// Returns true if `self` implicitly promotes to `target` under §5.2 rules. + pub fn promotes_to(&self, target: &Ty) -> bool { + // Refl + if self == target { + return true; + } + match (self, target) { + // Unsigned widening: same unsigned category, rank strictly increases + (a, b) if a.is_unsigned() && b.is_unsigned() => { + a.rank().unwrap_or(0) < b.rank().unwrap_or(0) + } + // Signed widening + (a, b) if a.is_signed() && b.is_signed() => { + a.rank().unwrap_or(0) < b.rank().unwrap_or(0) + } + // Float widening: F32 → F64 + (Ty::F32, Ty::F64) => true, + // Char: char → U if U is unsigned and rank(U) >= 3 (u32 or u64) + (Ty::Char, b) if b.is_unsigned() => b.rank().unwrap_or(0) >= 3, + // Ptr-Coerce: *mut T promotes to *T (same pointee) + ( + Ty::Ptr { mutable: true, pointee: pa }, + Ty::Ptr { mutable: false, pointee: pb }, + ) => pa == pb, + _ => false, + } + } + + /// Least upper bound under `promotes_to`. + /// Returns `Some(T)` where T is the common type, or `None` if incompatible. + pub fn common(a: &Ty, b: &Ty) -> Option { + if a.is_error() || b.is_error() { + return Some(Ty::Error); + } + if b.promotes_to(a) { + return Some(a.clone()); + } + if a.promotes_to(b) { + return Some(b.clone()); + } + None + } + + /// For pointer equality comparison. + pub fn ptr_common(a: &Ty, b: &Ty) -> Option { + match (a, b) { + ( + Ty::Ptr { mutable: ma, pointee: pa }, + Ty::Ptr { mutable: mb, pointee: pb }, + ) if pa == pb => Some(Ty::Ptr { + mutable: *ma && *mb, + pointee: pa.clone(), + }), + (Ty::OpaquePtr { mutable: ma }, Ty::OpaquePtr { mutable: mb }) => { + Some(Ty::OpaquePtr { mutable: *ma && *mb }) + } + _ => None, + } + } + + pub fn display(&self) -> String { + match self { + Ty::U8 => "u8".to_string(), + Ty::U16 => "u16".to_string(), + Ty::U32 => "u32".to_string(), + Ty::U64 => "u64".to_string(), + Ty::I8 => "i8".to_string(), + Ty::I16 => "i16".to_string(), + Ty::I32 => "i32".to_string(), + Ty::I64 => "i64".to_string(), + Ty::F32 => "f32".to_string(), + Ty::F64 => "f64".to_string(), + Ty::Bool => "bool".to_string(), + Ty::Char => "char".to_string(), + Ty::Unit => "()".to_string(), + Ty::Ptr { mutable: true, pointee } => format!("*mut {}", pointee.display()), + Ty::Ptr { mutable: false, pointee } => format!("*{}", pointee.display()), + Ty::OpaquePtr { mutable: true } => "*mut opaque".to_string(), + Ty::OpaquePtr { mutable: false } => "*opaque".to_string(), + Ty::Array { elem, size } => format!("[{}; {}]", elem.display(), size), + Ty::Struct(name) => format!("struct {}", name), + Ty::FnSig { params, ret } => { + let ps: Vec<_> = params.iter().map(|p| p.display()).collect(); + format!("fn({}) -> {}", ps.join(", "), ret.display()) + } + Ty::Error => "".to_string(), + } + } +} + // ── Operators ────────────────────────────────────────────────────────────────── #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -54,7 +236,7 @@ pub enum BinaryOp { Assign, // `=` } -// ── Types ────────────────────────────────────────────────────────────────────── +// ── Types (syntactic, from parser) ──────────────────────────────────────────── #[derive(Debug, Clone)] pub enum Type { @@ -74,6 +256,8 @@ pub enum Type { // Other primitives Bool, Char, + // Unit type (explicit `-> ()`) + Unit, // User-defined named type (e.g. a struct) Named(String, Span), // Typed pointer: `*type` (immutable) or `*mut type` (mutable) @@ -89,28 +273,29 @@ pub enum Type { // ── Struct literal field ─────────────────────────────────────────────────────── #[derive(Debug, Clone)] -pub struct StructField { +pub struct StructField { pub name: String, pub name_span: Span, - pub value: Expr, + pub value: Expr

, } // ── Expression ──────────────────────────────────────────────────────────────── #[derive(Debug, Clone)] -pub struct Expr { - pub kind: ExprKind, +pub struct Expr { + pub kind: ExprKind

, pub span: Span, + pub ty: P::ExprExtra, } -impl Expr { - pub fn new(kind: ExprKind, span: Span) -> Self { - Self { kind, span } +impl Expr { + pub fn new(kind: ExprKind, span: Span) -> Self { + Self { kind, span, ty: () } } } #[derive(Debug, Clone)] -pub enum ExprKind { +pub enum ExprKind { // Literals IntLit(String), FloatLit(String), @@ -125,46 +310,46 @@ pub enum ExprKind { StructLit { name: String, name_span: Span, - fields: Vec, + fields: Vec>, }, // Operators Unary { op: UnaryOp, op_span: Span, - expr: Box, + expr: Box>, }, Binary { op: BinaryOp, op_span: Span, - lhs: Box, - rhs: Box, + lhs: Box>, + rhs: Box>, }, // Compound assignment: `lhs op= rhs` (expands to `lhs = lhs op rhs`) CompoundAssign { op: CompoundAssignOp, op_span: Span, - lhs: Box, - rhs: Box, + lhs: Box>, + rhs: Box>, }, // Postfix Field { - expr: Box, + expr: Box>, field: String, field_span: Span, }, Index { - expr: Box, - index: Box, + expr: Box>, + index: Box>, }, Call { - callee: Box, - args: Vec, + callee: Box>, + args: Vec>, }, // Parenthesised expression - Group(Box), + Group(Box>), // Placeholder for parse errors — allows parsing to continue Error, @@ -173,57 +358,57 @@ pub enum ExprKind { // ── Block ────────────────────────────────────────────────────────────────────── #[derive(Debug, Clone)] -pub struct Block { - pub stmts: Vec, +pub struct Block { + pub stmts: Vec>, pub span: Span, } // ── Else branch ─────────────────────────────────────────────────────────────── #[derive(Debug, Clone)] -pub enum ElseBranch { - If(Box), // `else if …` - Block(Block), // `else { … }` +pub enum ElseBranch { + If(Box>), // `else if …` + Block(Block

), // `else { … }` } // ── Statement ───────────────────────────────────────────────────────────────── #[derive(Debug, Clone)] -pub struct Stmt { - pub kind: StmtKind, +pub struct Stmt { + pub kind: StmtKind

, pub span: Span, } #[derive(Debug, Clone)] -pub enum StmtKind { +pub enum StmtKind { /// `let [mut] name [: type] [= expr] ;` Let { mutable: bool, name: String, name_span: Span, ty: Option, - init: Option, + init: Option>, }, /// `return [expr] ;` - Return(Option), + Return(Option>), /// `if expr_ns block [else else_branch]` If { - cond: Expr, - then_block: Block, - else_branch: Option, + cond: Expr

, + then_block: Block

, + else_branch: Option>, }, /// `while expr_ns block` - While { cond: Expr, body: Block }, + While { cond: Expr

, body: Block

}, /// `loop block` - Loop { body: Block }, + Loop { body: Block

}, /// `break ;` Break, /// `continue ;` Continue, /// `{ stmts }` - Block(Block), + Block(Block

), /// `expr ;` - Expr(Expr), + Expr(Expr

), /// Error placeholder — emitted during recovery so the parent can continue. Error, } @@ -252,12 +437,12 @@ pub struct FieldDef { /// `fn name ( params ) [ -> type ] block` #[derive(Debug, Clone)] -pub struct FuncDef { +pub struct FuncDef { pub name: String, pub name_span: Span, pub params: Vec, pub ret_ty: Option, - pub body: Block, + pub body: Block

, } /// `struct name { fields }` @@ -269,14 +454,14 @@ pub struct StructDef { } #[derive(Debug, Clone)] -pub struct TopLevelDef { - pub kind: TopLevelDefKind, +pub struct TopLevelDef { + pub kind: TopLevelDefKind

, pub span: Span, } #[derive(Debug, Clone)] -pub enum TopLevelDefKind { - Func(FuncDef), +pub enum TopLevelDefKind { + Func(FuncDef

), Struct(StructDef), /// Error placeholder for recovery. Error, @@ -284,7 +469,7 @@ pub enum TopLevelDefKind { /// The root of the AST — a sequence of top-level definitions. #[derive(Debug, Clone)] -pub struct Program { - pub defs: Vec, +pub struct Program { + pub defs: Vec>, pub span: Span, } diff --git a/fluxc/src/checker/env.rs b/fluxc/src/checker/env.rs new file mode 100644 index 0000000..f7cc970 --- /dev/null +++ b/fluxc/src/checker/env.rs @@ -0,0 +1,158 @@ +use std::collections::{HashMap, HashSet}; +use crate::ast::Ty; +use crate::token::Span; + +// ── StructTable ──────────────────────────────────────────────────────────────── + +pub struct StructTable { + entries: HashMap, +} + +struct StructEntry { + name_span: Span, + fields: Vec, +} + +pub struct FieldEntry { + pub name: String, + pub name_span: Span, + pub ty: Ty, + pub ty_span: Span, +} + +impl StructTable { + pub fn new() -> Self { + Self { entries: HashMap::new() } + } + + /// Insert a struct name; returns false if it was already present. + pub fn insert_name(&mut self, name: &str, span: Span) -> bool { + if self.entries.contains_key(name) { + return false; + } + self.entries.insert(name.to_string(), StructEntry { name_span: span, fields: Vec::new() }); + true + } + + pub fn contains(&self, name: &str) -> bool { + self.entries.contains_key(name) + } + + pub fn add_field(&mut self, struct_name: &str, field: FieldEntry) { + if let Some(entry) = self.entries.get_mut(struct_name) { + entry.fields.push(field); + } + } + + pub fn fields(&self, name: &str) -> Option<&[FieldEntry]> { + self.entries.get(name).map(|e| e.fields.as_slice()) + } + + pub fn name_span(&self, name: &str) -> Option { + self.entries.get(name).map(|e| e.name_span) + } + + pub fn field_ty(&self, struct_name: &str, field_name: &str) -> Option<&Ty> { + self.entries.get(struct_name)?.fields.iter().find(|f| f.name == field_name).map(|f| &f.ty) + } + + pub fn names_in_outer(&self, _saved: usize) -> HashSet { + self.entries.keys().cloned().collect() + } + + pub fn all_struct_names(&self) -> Vec { + self.entries.keys().cloned().collect() + } + + pub fn all_entries(&self) -> impl Iterator { + self.entries.iter().map(|(k, v)| (k.as_str(), v)) + } +} + +// ── FuncTable ────────────────────────────────────────────────────────────────── + +pub struct FuncTable { + entries: HashMap, +} + +pub struct FuncEntry { + pub name_span: Span, + pub params: Vec, + pub ret: Ty, +} + +pub struct ParamEntry { + pub name: String, + pub name_span: Span, + pub ty: Ty, + pub mutable: bool, +} + +impl FuncTable { + pub fn new() -> Self { + Self { entries: HashMap::new() } + } + + /// Insert a function; returns false if the name was already present. + pub fn insert(&mut self, name: &str, span: Span, params: Vec, ret: Ty) -> bool { + if self.entries.contains_key(name) { + return false; + } + self.entries.insert(name.to_string(), FuncEntry { name_span: span, params, ret }); + true + } + + pub fn get(&self, name: &str) -> Option<&FuncEntry> { + self.entries.get(name) + } + + pub fn contains(&self, name: &str) -> bool { + self.entries.contains_key(name) + } +} + +// ── TypeEnv ─────────────────────────────────────────────────────────────────── + +#[derive(Clone)] +pub struct TypeEnv { + bindings: Vec, +} + +#[derive(Clone)] +pub struct Binding { + pub name: String, + pub ty: Ty, + pub mutable: bool, +} + +impl TypeEnv { + pub fn new() -> Self { + Self { bindings: Vec::new() } + } + + pub fn extend(&mut self, name: String, ty: Ty, mutable: bool) { + self.bindings.push(Binding { name, ty, mutable }); + } + + /// Look up a binding; rightmost (most recent) binding wins. + pub fn lookup(&self, name: &str) -> Option<(&Ty, bool)> { + self.bindings.iter().rev().find(|b| b.name == name).map(|b| (&b.ty, b.mutable)) + } + + pub fn contains(&self, name: &str) -> bool { + self.bindings.iter().any(|b| b.name == name) + } + + pub fn save(&self) -> usize { + self.bindings.len() + } + + pub fn restore(&mut self, saved: usize) { + self.bindings.truncate(saved); + } + + /// Returns all binding names that were introduced after `saved`. + pub fn names_in_outer(&self, saved: usize) -> HashSet { + self.bindings[saved..].iter().map(|b| b.name.clone()).collect() + } +} diff --git a/fluxc/src/checker/expr.rs b/fluxc/src/checker/expr.rs new file mode 100644 index 0000000..8d48c1a --- /dev/null +++ b/fluxc/src/checker/expr.rs @@ -0,0 +1,677 @@ +use std::collections::HashSet; + +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`. + /// 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 → i32 + ExprKind::IntLit(_) => Ty::I32, + + // 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 => { + 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)), + ); + } + 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() && !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 + 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 matching integer types + 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 + 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) + } + } + } + + /// 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, + 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, + } +} diff --git a/fluxc/src/checker/mod.rs b/fluxc/src/checker/mod.rs new file mode 100644 index 0000000..50f97ea --- /dev/null +++ b/fluxc/src/checker/mod.rs @@ -0,0 +1,276 @@ +pub mod env; +pub mod expr; +pub mod stmt; + +use std::collections::HashSet; + +use crate::ast::{self, Parsed, Ty, Type}; +use crate::diagnostics::{Diagnostic, Label}; +use crate::token::Span; +use env::{FieldEntry, FuncTable, ParamEntry, StructTable}; + +// ── Checker ──────────────────────────────────────────────────────────────────── + +pub struct Checker { + pub sigma: StructTable, + pub phi: FuncTable, + pub errors: Vec, +} + +impl Checker { + fn new() -> Self { + let mut sigma = StructTable::new(); + let phi = FuncTable::new(); + + // Pre-load `string_view` built-in (§3.1): { data: *char, size: u64 } + sigma.insert_name("string_view", Span::new(0, 0)); + sigma.add_field( + "string_view", + FieldEntry { + name: "data".to_string(), + name_span: Span::new(0, 0), + ty: Ty::Ptr { mutable: false, pointee: Box::new(Ty::Char) }, + ty_span: Span::new(0, 0), + }, + ); + sigma.add_field( + "string_view", + FieldEntry { + name: "size".to_string(), + name_span: Span::new(0, 0), + ty: Ty::U64, + ty_span: Span::new(0, 0), + }, + ); + + Self { sigma, phi, errors: Vec::new() } + } + + pub fn emit(&mut self, diag: Diagnostic) { + self.errors.push(diag); + } + + /// Resolve a syntactic `ast::Type` to a semantic `Ty`. + pub fn resolve_type(&mut self, ty: &Type, span: Span) -> Ty { + match ty { + Type::U8 => Ty::U8, + Type::U16 => Ty::U16, + Type::U32 => Ty::U32, + Type::U64 => Ty::U64, + Type::I8 => Ty::I8, + Type::I16 => Ty::I16, + Type::I32 => Ty::I32, + Type::I64 => Ty::I64, + Type::F32 => Ty::F32, + Type::F64 => Ty::F64, + Type::Bool => Ty::Bool, + Type::Char => Ty::Char, + Type::Unit => Ty::Unit, + Type::Named(name, name_span) => { + if self.sigma.contains(name) { + Ty::Struct(name.clone()) + } else { + self.emit( + Diagnostic::error(format!("undefined type `{name}`")) + .with_label(Label::primary(*name_span)), + ); + Ty::Error + } + } + Type::Pointer { mutable, pointee } => { + let inner = self.resolve_type(pointee, span); + Ty::Ptr { mutable: *mutable, pointee: Box::new(inner) } + } + Type::OpaquePointer { mutable } => Ty::OpaquePtr { mutable: *mutable }, + Type::Array { elem, size } => { + let elem_ty = self.resolve_type(elem, span); + match size.parse::() { + Ok(n) => Ty::Array { elem: Box::new(elem_ty), size: n }, + Err(_) => { + self.emit( + Diagnostic::error(format!("invalid array size `{size}`")) + .with_label(Label::primary(span)), + ); + Ty::Error + } + } + } + Type::Error => Ty::Error, + } + } + + // ── Size-cycle detection ─────────────────────────────────────────────────── + + fn has_size_cycle( + &self, + name: &str, + gray: &mut HashSet, + black: &mut HashSet, + ) -> bool { + if black.contains(name) { + return false; + } + if gray.contains(name) { + return true; + } + gray.insert(name.to_string()); + + // Collect field types to avoid holding an immutable borrow on self.sigma + // while recursing (which needs immutable access again). + let field_tys: Vec = self + .sigma + .fields(name) + .unwrap_or(&[]) + .iter() + .map(|f| f.ty.clone()) + .collect(); + + for ty in &field_tys { + if let Some(inner) = value_struct_name(ty) { + if self.has_size_cycle(inner, gray, black) { + gray.remove(name); + return true; + } + } + } + + gray.remove(name); + black.insert(name.to_string()); + false + } +} + +/// Returns the struct name embedded by-value in `ty` (if any). +/// Pointer-to-struct is NOT by-value, so it does not cause a size cycle. +fn value_struct_name(ty: &Ty) -> Option<&str> { + match ty { + Ty::Struct(name) => Some(name.as_str()), + Ty::Array { elem, .. } => value_struct_name(elem), + _ => None, + } +} + +// ── Entry point ──────────────────────────────────────────────────────────────── + +pub fn check(program: &ast::Program) -> Vec { + let mut checker = Checker::new(); + + // ── Pass 1: collect struct names + function signatures ──────────────────── + for def in &program.defs { + match &def.kind { + ast::TopLevelDefKind::Struct(s) => { + if s.name == "string_view" { + checker.emit( + Diagnostic::error("`string_view` is a reserved built-in name") + .with_label(Label::primary(s.name_span)), + ); + } else if !checker.sigma.insert_name(&s.name, s.name_span) { + checker.emit( + Diagnostic::error(format!("duplicate struct `{}`", s.name)) + .with_label(Label::primary(s.name_span)), + ); + } + } + ast::TopLevelDefKind::Func(f) => { + let params: Vec = f + .params + .iter() + .map(|p| { + let ty = checker.resolve_type(&p.ty, p.name_span); + ParamEntry { + name: p.name.clone(), + name_span: p.name_span, + ty, + mutable: p.mutable, + } + }) + .collect(); + let ret = match &f.ret_ty { + Some(t) => checker.resolve_type(t, f.name_span), + None => Ty::Unit, + }; + if !checker.phi.insert(&f.name, f.name_span, params, ret) { + checker.emit( + Diagnostic::error(format!("duplicate function `{}`", f.name)) + .with_label(Label::primary(f.name_span)), + ); + } + } + ast::TopLevelDefKind::Error => {} + } + } + + // ── Pass 2: resolve struct field types + check for size cycles ──────────── + for def in &program.defs { + if let ast::TopLevelDefKind::Struct(s) = &def.kind { + if s.name == "string_view" { + continue; // built-in, already populated + } + for field in &s.fields { + let ty = checker.resolve_type(&field.ty, field.name_span); + checker.sigma.add_field( + &s.name, + FieldEntry { + name: field.name.clone(), + name_span: field.name_span, + ty, + ty_span: field.name_span, + }, + ); + } + } + } + + let struct_names = checker.sigma.all_struct_names(); + let mut black = HashSet::new(); + for name in struct_names { + let mut gray = HashSet::new(); + if checker.has_size_cycle(&name, &mut gray, &mut black) { + if let Some(span) = checker.sigma.name_span(&name) { + checker.emit( + Diagnostic::error(format!( + "struct `{name}` has infinite size (recursive by value)" + )) + .with_label(Label::primary(span)), + ); + } + } + } + + // ── Pass 3: check function bodies ───────────────────────────────────────── + for def in &program.defs { + if let ast::TopLevelDefKind::Func(f) = &def.kind { + checker.check_function(f); + } + } + + // ── Pass 4: verify entry point ──────────────────────────────────────────── + let main_info = checker + .phi + .get("main") + .map(|e| (e.name_span, e.params.len(), e.ret.clone())); + match main_info { + None => { + checker.emit( + Diagnostic::error("program has no `main` function") + .with_label(Label::primary(program.span)), + ); + } + Some((name_span, param_count, ret)) => { + if param_count != 0 { + checker.emit( + Diagnostic::error("`main` must take no parameters") + .with_label(Label::primary(name_span)), + ); + } + if ret != Ty::Unit && ret != Ty::I32 && !ret.is_error() { + checker.emit( + Diagnostic::error("`main` must return `()` or `i32`") + .with_label(Label::primary(name_span)), + ); + } + } + } + + checker.errors +} diff --git a/fluxc/src/checker/stmt.rs b/fluxc/src/checker/stmt.rs new file mode 100644 index 0000000..146c4c4 --- /dev/null +++ b/fluxc/src/checker/stmt.rs @@ -0,0 +1,365 @@ +use std::collections::HashSet; + +use crate::ast::{self, BinaryOp, ElseBranch, ExprKind, Parsed, Ty}; +use crate::diagnostics::{Diagnostic, Label}; +use super::env::TypeEnv; +use super::Checker; + +// ── Control flow ────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Cf { + Normal, + Diverges, +} + +// ── Check context ───────────────────────────────────────────────────────────── + +pub struct CheckCtx { + pub ret_ty: Ty, + pub in_loop: bool, +} + +impl CheckCtx { + fn loop_ctx(&self) -> CheckCtx { + CheckCtx { ret_ty: self.ret_ty.clone(), in_loop: true } + } +} + +// ── Function entry ──────────────────────────────────────────────────────────── + +impl Checker { + pub fn check_function(&mut self, f: &ast::FuncDef) { + let (params_info, ret_ty) = { + let entry = self.phi.get(&f.name).expect("function must be in phi after pass 1"); + let params: Vec<(String, Ty, bool)> = entry + .params + .iter() + .map(|p| (p.name.clone(), p.ty.clone(), p.mutable)) + .collect(); + (params, entry.ret.clone()) + }; + + let mut env = TypeEnv::new(); + let mut assigned: HashSet = HashSet::new(); + + for (name, ty, mutable) in params_info { + env.extend(name.clone(), ty, mutable); + assigned.insert(name); + } + + let ctx = CheckCtx { ret_ty: ret_ty.clone(), in_loop: false }; + let cf = self.check_block(&f.body, &mut env, &mut assigned, &ctx); + + // If the declared return type is non-unit and the body may not diverge, + // emit a missing-return error. + if ret_ty != Ty::Unit && !ret_ty.is_error() && cf != Cf::Diverges { + self.emit( + Diagnostic::error(format!( + "function `{}` must always return a value of type `{}`", + f.name, + ret_ty.display() + )) + .with_label(Label::primary(f.body.span)), + ); + } + } + + // ── Block ───────────────────────────────────────────────────────────────── + + pub fn check_block( + &mut self, + block: &ast::Block, + env: &mut TypeEnv, + assigned: &mut HashSet, + ctx: &CheckCtx, + ) -> Cf { + let saved = env.save(); + let mut cf = Cf::Normal; + + for stmt in &block.stmts { + if cf == Cf::Diverges { + self.emit( + Diagnostic::warning("unreachable statement") + .with_label(Label::primary(stmt.span)), + ); + break; + } + cf = self.check_stmt(stmt, env, assigned, ctx); + } + + // Remove block-local variables from assigned (they leave scope). + for var in env.names_in_outer(saved) { + assigned.remove(&var); + } + env.restore(saved); + cf + } + + // ── Statement ───────────────────────────────────────────────────────────── + + fn check_stmt( + &mut self, + stmt: &ast::Stmt, + env: &mut TypeEnv, + assigned: &mut HashSet, + ctx: &CheckCtx, + ) -> Cf { + match &stmt.kind { + // T-Let + ast::StmtKind::Let { mutable, name, name_span, ty, init } => { + let ann_ty = ty.as_ref().map(|t| self.resolve_type(t, *name_span)); + + let init_ty = init.as_ref().map(|e| { + let t = self.check_expr(e, env, assigned); + // Propagate inner assignments (e.g. `let x = (y = 5);`) + for var in collect_assigns(e) { + assigned.insert(var); + } + t + }); + + let init_span = init.as_ref().map(|e| e.span).unwrap_or(*name_span); + let resolved = match (ann_ty, init_ty) { + (Some(ann), Some(ref init_t)) => { + if !init_t.is_error() && !ann.is_error() && !init_t.promotes_to(&ann) { + self.emit( + Diagnostic::error(format!( + "type mismatch in `let`: expected `{}`, got `{}`", + ann.display(), + init_t.display() + )) + .with_label(Label::primary(init_span)), + ); + } + ann + } + (Some(ann), None) => ann, + (None, Some(init)) => init, + (None, None) => { + self.emit( + Diagnostic::error(format!( + "cannot infer type of `{name}`: add a type annotation or initialiser" + )) + .with_label(Label::primary(*name_span)), + ); + Ty::Error + } + }; + + env.extend(name.clone(), resolved, *mutable); + if init.is_some() { + assigned.insert(name.clone()); + } + Cf::Normal + } + + // T-Return + ast::StmtKind::Return(expr) => { + match expr { + Some(e) => { + let ty = self.check_expr(e, env, assigned); + if ctx.ret_ty == Ty::Unit { + if !ty.is_error() && ty != Ty::Unit { + self.emit( + Diagnostic::error(format!( + "this function returns `()`, cannot return `{}`", + ty.display() + )) + .with_label(Label::primary(e.span)), + ); + } + } else if !ty.is_error() + && !ctx.ret_ty.is_error() + && !ty.promotes_to(&ctx.ret_ty) + { + self.emit( + Diagnostic::error(format!( + "type mismatch: expected `{}`, got `{}`", + ctx.ret_ty.display(), + ty.display() + )) + .with_label(Label::primary(e.span)), + ); + } + } + None => { + if ctx.ret_ty != Ty::Unit && !ctx.ret_ty.is_error() { + self.emit( + Diagnostic::error(format!( + "missing return value: function must return `{}`", + ctx.ret_ty.display() + )) + .with_label(Label::primary(stmt.span)), + ); + } + } + } + Cf::Diverges + } + + // T-If + ast::StmtKind::If { cond, then_block, else_branch } => { + let cond_ty = self.check_expr(cond, env, assigned); + if !cond_ty.is_error() && cond_ty != Ty::Bool { + self.emit( + Diagnostic::error(format!( + "condition must be `bool`, got `{}`", + cond_ty.display() + )) + .with_label(Label::primary(cond.span)), + ); + } + // Propagate assigns from condition expression + for var in collect_assigns(cond) { + assigned.insert(var); + } + + let mut a_then = assigned.clone(); + let cf_then = self.check_block(then_block, env, &mut a_then, ctx); + + let cf_else = match else_branch { + Some(ElseBranch::Block(b)) => { + let mut a_else = assigned.clone(); + let cf = self.check_block(b, env, &mut a_else, ctx); + // A_out = A_then ∩ A_else (both branches definitely assign) + *assigned = a_then.intersection(&a_else).cloned().collect(); + cf + } + Some(ElseBranch::If(if_stmt)) => { + let mut a_else = assigned.clone(); + let cf = self.check_stmt(if_stmt, env, &mut a_else, ctx); + *assigned = a_then.intersection(&a_else).cloned().collect(); + cf + } + None => { + // No else: no new definite assignments (branch might not run) + Cf::Normal + } + }; + + if cf_then == Cf::Diverges && cf_else == Cf::Diverges { + Cf::Diverges + } else { + Cf::Normal + } + } + + // T-While + ast::StmtKind::While { cond, body } => { + let cond_ty = self.check_expr(cond, env, assigned); + if !cond_ty.is_error() && cond_ty != Ty::Bool { + self.emit( + Diagnostic::error(format!( + "while condition must be `bool`, got `{}`", + cond_ty.display() + )) + .with_label(Label::primary(cond.span)), + ); + } + for var in collect_assigns(cond) { + assigned.insert(var); + } + let loop_ctx = ctx.loop_ctx(); + let mut a_body = assigned.clone(); + self.check_block(body, env, &mut a_body, &loop_ctx); + // Conservatively Normal: condition might be false on first iteration + Cf::Normal + } + + // T-Loop + ast::StmtKind::Loop { body } => { + let loop_ctx = ctx.loop_ctx(); + let mut a_body = assigned.clone(); + self.check_block(body, env, &mut a_body, &loop_ctx); + // Conservatively Normal: might break + Cf::Normal + } + + ast::StmtKind::Break => { + if !ctx.in_loop { + self.emit( + Diagnostic::error("`break` outside of a loop") + .with_label(Label::primary(stmt.span)), + ); + } + Cf::Diverges + } + + ast::StmtKind::Continue => { + if !ctx.in_loop { + self.emit( + Diagnostic::error("`continue` outside of a loop") + .with_label(Label::primary(stmt.span)), + ); + } + Cf::Diverges + } + + ast::StmtKind::Block(block) => self.check_block(block, env, assigned, ctx), + + // T-ExprStmt + ast::StmtKind::Expr(expr) => { + self.check_expr(expr, env, assigned); + for var in collect_assigns(expr) { + assigned.insert(var); + } + Cf::Normal + } + + ast::StmtKind::Error => Cf::Normal, + } + } +} + +// ── assigns(e) — §8.5 ──────────────────────────────────────────────────────── + +/// Collect the set of variable names that are directly (re-)assigned as a +/// side-effect of evaluating `expr`. Only simple `ident = …` assignments +/// contribute; compound places (`s.f = …`) are excluded. +pub fn collect_assigns(expr: &ast::Expr) -> HashSet { + let mut set = HashSet::new(); + collect_assigns_inner(expr, &mut set); + set +} + +fn collect_assigns_inner(expr: &ast::Expr, set: &mut HashSet) { + match &expr.kind { + ExprKind::Binary { op, lhs, rhs, .. } => { + if *op == BinaryOp::Assign { + collect_assigns_inner(rhs, set); + if let ExprKind::Ident(name) = &lhs.kind { + set.insert(name.clone()); + } + } else { + collect_assigns_inner(lhs, set); + collect_assigns_inner(rhs, set); + } + } + ExprKind::CompoundAssign { rhs, .. } => { + // Compound assign requires lhs to already be assigned (read side), + // so we only propagate from rhs. + collect_assigns_inner(rhs, set); + } + ExprKind::StructLit { fields, .. } => { + for f in fields { + collect_assigns_inner(&f.value, set); + } + } + ExprKind::Group(inner) => collect_assigns_inner(inner, set), + ExprKind::Unary { expr, .. } => collect_assigns_inner(expr, set), + ExprKind::Field { expr, .. } => collect_assigns_inner(expr, set), + ExprKind::Index { expr, index } => { + collect_assigns_inner(expr, set); + collect_assigns_inner(index, set); + } + ExprKind::Call { callee, args } => { + collect_assigns_inner(callee, set); + for a in args { + collect_assigns_inner(a, set); + } + } + // Leaves (literals, ident, bool, error) don't assign anything + _ => {} + } +} diff --git a/fluxc/src/main.rs b/fluxc/src/main.rs index aa9355c..7b46711 100644 --- a/fluxc/src/main.rs +++ b/fluxc/src/main.rs @@ -3,6 +3,7 @@ use std::{fs, process}; use crate::parser::Parser; pub mod ast; +pub mod checker; pub mod cli; pub mod diagnostics; pub mod lexer; @@ -24,7 +25,13 @@ fn main() { had_errors = true; } - println!("{program:#?}"); + if parser.errors.is_empty() { + let sema_errors = checker::check(&program); + for diag in &sema_errors { + eprint!("{}", diag.render(&content, path)); + had_errors = true; + } + } } if had_errors {