From 1f3d64f97c10fd71874d2ce4e7589bae65af1339 Mon Sep 17 00:00:00 2001 From: Jooris Hadeler Date: Wed, 11 Mar 2026 15:16:49 +0100 Subject: [PATCH] Test: fix parser generic annotations and add checker test suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix two test helpers (top/program) missing 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 --- fluxc/src/ast.rs | 36 ++- fluxc/src/checker/env.rs | 51 +++- fluxc/src/checker/expr.rs | 134 ++++++--- fluxc/src/checker/mod.rs | 22 +- fluxc/src/checker/stmt.rs | 33 ++- fluxc/src/checker/tests.rs | 556 +++++++++++++++++++++++++++++++++++++ fluxc/src/parser.rs | 78 +++--- 7 files changed, 812 insertions(+), 98 deletions(-) create mode 100644 fluxc/src/checker/tests.rs diff --git a/fluxc/src/ast.rs b/fluxc/src/ast.rs index c312711..59a5dd6 100644 --- a/fluxc/src/ast.rs +++ b/fluxc/src/ast.rs @@ -113,8 +113,14 @@ impl Ty { (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 }, + Ty::Ptr { + mutable: true, + pointee: pa, + }, + Ty::Ptr { + mutable: false, + pointee: pb, + }, ) => pa == pb, _ => false, } @@ -139,15 +145,21 @@ impl Ty { pub fn ptr_common(a: &Ty, b: &Ty) -> Option { match (a, b) { ( - Ty::Ptr { mutable: ma, pointee: pa }, - Ty::Ptr { mutable: mb, pointee: pb }, + 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 }) - } + (Ty::OpaquePtr { mutable: ma }, Ty::OpaquePtr { mutable: mb }) => Some(Ty::OpaquePtr { + mutable: *ma && *mb, + }), _ => None, } } @@ -167,8 +179,14 @@ impl Ty { 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::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), diff --git a/fluxc/src/checker/env.rs b/fluxc/src/checker/env.rs index f7cc970..be26542 100644 --- a/fluxc/src/checker/env.rs +++ b/fluxc/src/checker/env.rs @@ -1,6 +1,6 @@ -use std::collections::{HashMap, HashSet}; use crate::ast::Ty; use crate::token::Span; +use std::collections::{HashMap, HashSet}; // ── StructTable ──────────────────────────────────────────────────────────────── @@ -8,7 +8,7 @@ pub struct StructTable { entries: HashMap, } -struct StructEntry { +pub struct StructEntry { name_span: Span, fields: Vec, } @@ -22,7 +22,9 @@ pub struct FieldEntry { impl StructTable { pub fn new() -> Self { - Self { entries: HashMap::new() } + Self { + entries: HashMap::new(), + } } /// Insert a struct name; returns false if it was already present. @@ -30,7 +32,13 @@ impl StructTable { if self.entries.contains_key(name) { return false; } - self.entries.insert(name.to_string(), StructEntry { name_span: span, fields: Vec::new() }); + self.entries.insert( + name.to_string(), + StructEntry { + name_span: span, + fields: Vec::new(), + }, + ); true } @@ -53,7 +61,12 @@ impl StructTable { } 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) + 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 { @@ -90,7 +103,9 @@ pub struct ParamEntry { impl FuncTable { pub fn new() -> Self { - Self { entries: HashMap::new() } + Self { + entries: HashMap::new(), + } } /// Insert a function; returns false if the name was already present. @@ -98,7 +113,14 @@ impl FuncTable { if self.entries.contains_key(name) { return false; } - self.entries.insert(name.to_string(), FuncEntry { name_span: span, params, ret }); + self.entries.insert( + name.to_string(), + FuncEntry { + name_span: span, + params, + ret, + }, + ); true } @@ -127,7 +149,9 @@ pub struct Binding { impl TypeEnv { pub fn new() -> Self { - Self { bindings: Vec::new() } + Self { + bindings: Vec::new(), + } } pub fn extend(&mut self, name: String, ty: Ty, mutable: bool) { @@ -136,7 +160,11 @@ impl TypeEnv { /// 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)) + self.bindings + .iter() + .rev() + .find(|b| b.name == name) + .map(|b| (&b.ty, b.mutable)) } pub fn contains(&self, name: &str) -> bool { @@ -153,6 +181,9 @@ impl TypeEnv { /// 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() + self.bindings[saved..] + .iter() + .map(|b| b.name.clone()) + .collect() } } diff --git a/fluxc/src/checker/expr.rs b/fluxc/src/checker/expr.rs index 8d48c1a..933cf4b 100644 --- a/fluxc/src/checker/expr.rs +++ b/fluxc/src/checker/expr.rs @@ -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, + 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 { @@ -596,7 +654,9 @@ impl Checker { pub fn is_place(&self, expr: &ast::Expr, 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, 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, } } diff --git a/fluxc/src/checker/mod.rs b/fluxc/src/checker/mod.rs index 50f97ea..68f2af5 100644 --- a/fluxc/src/checker/mod.rs +++ b/fluxc/src/checker/mod.rs @@ -1,6 +1,7 @@ pub mod env; pub mod expr; pub mod stmt; +mod tests; use std::collections::HashSet; @@ -29,7 +30,10 @@ impl Checker { FieldEntry { name: "data".to_string(), name_span: Span::new(0, 0), - ty: Ty::Ptr { mutable: false, pointee: Box::new(Ty::Char) }, + ty: Ty::Ptr { + mutable: false, + pointee: Box::new(Ty::Char), + }, ty_span: Span::new(0, 0), }, ); @@ -43,7 +47,11 @@ impl Checker { }, ); - Self { sigma, phi, errors: Vec::new() } + Self { + sigma, + phi, + errors: Vec::new(), + } } pub fn emit(&mut self, diag: Diagnostic) { @@ -79,13 +87,19 @@ impl Checker { } Type::Pointer { mutable, pointee } => { let inner = self.resolve_type(pointee, span); - Ty::Ptr { mutable: *mutable, pointee: Box::new(inner) } + 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 }, + Ok(n) => Ty::Array { + elem: Box::new(elem_ty), + size: n, + }, Err(_) => { self.emit( Diagnostic::error(format!("invalid array size `{size}`")) diff --git a/fluxc/src/checker/stmt.rs b/fluxc/src/checker/stmt.rs index 146c4c4..6163ab2 100644 --- a/fluxc/src/checker/stmt.rs +++ b/fluxc/src/checker/stmt.rs @@ -1,9 +1,9 @@ use std::collections::HashSet; +use super::Checker; +use super::env::TypeEnv; use crate::ast::{self, BinaryOp, ElseBranch, ExprKind, Parsed, Ty}; use crate::diagnostics::{Diagnostic, Label}; -use super::env::TypeEnv; -use super::Checker; // ── Control flow ────────────────────────────────────────────────────────────── @@ -22,7 +22,10 @@ pub struct CheckCtx { impl CheckCtx { fn loop_ctx(&self) -> CheckCtx { - CheckCtx { ret_ty: self.ret_ty.clone(), in_loop: true } + CheckCtx { + ret_ty: self.ret_ty.clone(), + in_loop: true, + } } } @@ -31,7 +34,10 @@ impl CheckCtx { 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 entry = self + .phi + .get(&f.name) + .expect("function must be in phi after pass 1"); let params: Vec<(String, Ty, bool)> = entry .params .iter() @@ -48,7 +54,10 @@ impl Checker { assigned.insert(name); } - let ctx = CheckCtx { ret_ty: ret_ty.clone(), in_loop: false }; + 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, @@ -107,7 +116,13 @@ impl Checker { ) -> Cf { match &stmt.kind { // T-Let - ast::StmtKind::Let { mutable, name, name_span, ty, init } => { + 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| { @@ -199,7 +214,11 @@ impl Checker { } // T-If - ast::StmtKind::If { cond, then_block, else_branch } => { + 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( diff --git a/fluxc/src/checker/tests.rs b/fluxc/src/checker/tests.rs new file mode 100644 index 0000000..e65a194 --- /dev/null +++ b/fluxc/src/checker/tests.rs @@ -0,0 +1,556 @@ +/// Checker integration tests. +/// +/// Each helper parses a source string and runs the full 4-pass checker, +/// returning the list of error messages (without ANSI codes) so tests can +/// assert on their content. +#[cfg(test)] +mod tests { + use crate::checker; + use crate::parser::Parser; + + // ── Helpers ─────────────────────────────────────────────────────────────── + + /// Run the checker on `src` and return all diagnostic messages. + fn errors(src: &str) -> Vec { + let mut parser = Parser::new(src); + let program = parser.parse_program(); + assert!( + parser.errors.is_empty(), + "parse errors: {:?}", + parser.errors + ); + checker::check(&program) + .into_iter() + .map(|d| d.message) + .collect() + } + + /// Assert that checking `src` produces no errors. + fn ok(src: &str) { + let msgs = errors(src); + assert!(msgs.is_empty(), "unexpected errors: {:?}", msgs); + } + + /// Assert that checking `src` produces at least one error whose message + /// contains `needle`. + fn err_contains(src: &str, needle: &str) { + let msgs = errors(src); + assert!( + msgs.iter().any(|m| m.contains(needle)), + "expected error containing {:?}, got: {:?}", + needle, + msgs + ); + } + + // ── Pass 4: entry point ─────────────────────────────────────────────────── + + #[test] + fn no_main_is_error() { + err_contains("fn foo() { }", "no `main`"); + } + + #[test] + fn main_with_params_is_error() { + err_contains("fn main(x: i32) { }", "no parameters"); + } + + #[test] + fn main_bad_return_type_is_error() { + err_contains("fn main() -> bool { return true; }", "return `()` or `i32`"); + } + + #[test] + fn main_unit_return_ok() { + ok("fn main() { }"); + } + + #[test] + fn main_i32_return_ok() { + ok("fn main() -> i32 { return 0; }"); + } + + // ── Pass 1: duplicate definitions ───────────────────────────────────────── + + #[test] + fn duplicate_function_is_error() { + err_contains( + "fn foo() { } fn foo() { } fn main() { }", + "duplicate function `foo`", + ); + } + + #[test] + fn duplicate_struct_is_error() { + err_contains( + "struct S { x: i32 } struct S { y: i32 } fn main() { }", + "duplicate struct `S`", + ); + } + + #[test] + fn string_view_reserved() { + err_contains("struct string_view { } fn main() { }", "reserved built-in"); + } + + // ── Pass 2: struct fields + cycles ──────────────────────────────────────── + + #[test] + fn undefined_field_type_is_error() { + err_contains( + "struct S { x: Unknown } fn main() { }", + "undefined type `Unknown`", + ); + } + + #[test] + fn struct_size_cycle_is_error() { + err_contains( + "struct A { b: B } struct B { a: A } fn main() { }", + "infinite size", + ); + } + + #[test] + fn struct_pointer_cycle_ok() { + // Pointer-to-self does NOT cause a size cycle + ok("struct Node { next: *Node, value: i32 } fn main() { }"); + } + + // ── Literals ────────────────────────────────────────────────────────────── + + #[test] + fn int_literal_is_i32() { + ok("fn main() { let x: i32 = 42; }"); + } + + #[test] + fn float_literal_is_f64() { + ok("fn main() { let x: f64 = 3.14; }"); + } + + #[test] + fn bool_literal() { + ok("fn main() { let x: bool = true; }"); + } + + #[test] + fn char_literal() { + ok("fn main() { let x: char = 'a'; }"); + } + + // ── Implicit promotion in let ────────────────────────────────────────────── + + #[test] + fn i8_promotes_to_i32_in_let() { + // i32 literal (type i32) promotes to i64 + ok("fn main() { let x: i64 = 1; }"); + } + + #[test] + fn f32_promotes_to_f64_in_let() { + ok("fn foo(v: f32) { let x: f64 = v; } fn main() { }"); + } + + #[test] + fn cross_category_forbidden_in_let() { + err_contains("fn main() { let x: f64 = 1; }", "type mismatch"); + } + + #[test] + fn unsigned_to_signed_forbidden_in_let() { + err_contains( + "fn foo(v: u32) { let x: i32 = v; } fn main() { }", + "type mismatch", + ); + } + + // ── Type inference ──────────────────────────────────────────────────────── + + #[test] + fn let_type_inferred_from_init() { + // `let x = 1;` → inferred as i32; later usage as i32 should pass + ok("fn add(a: i32, b: i32) -> i32 { return a + b; } + fn main() { let x = 1; let y = add(x, 2); }"); + } + + #[test] + fn let_no_type_no_init_is_error() { + err_contains("fn main() { let x; }", "cannot infer type"); + } + + // ── Definite assignment ─────────────────────────────────────────────────── + + #[test] + fn use_before_init_is_error() { + err_contains( + "fn main() { let x: i32; let y = x; }", + "uninitialized variable `x`", + ); + } + + #[test] + fn init_via_assignment_is_ok() { + ok("fn main() { let mut x: i32; x = 5; let y = x; }"); + } + + #[test] + fn definite_assign_both_branches() { + ok("fn f(b: bool) -> i32 { + let mut x: i32; + if b { x = 1; } else { x = 2; } + return x; + } fn main() { }"); + } + + #[test] + fn definite_assign_missing_else_branch_is_error() { + err_contains( + "fn f(b: bool) -> i32 { + let x: i32; + if b { x = 1; } + return x; + } fn main() { }", + "uninitialized variable `x`", + ); + } + + // ── Undefined variable / function ───────────────────────────────────────── + + #[test] + fn undefined_variable_is_error() { + err_contains("fn main() { let y = x; }", "undefined variable `x`"); + } + + #[test] + fn undefined_function_is_error() { + err_contains("fn main() { foo(); }", "undefined function `foo`"); + } + + // ── Arithmetic ──────────────────────────────────────────────────────────── + + #[test] + fn arithmetic_same_type_ok() { + ok("fn main() { let x: i32 = 1 + 2; }"); + } + + #[test] + fn arithmetic_cross_category_is_error() { + // float + int: not in same category + err_contains( + "fn f(a: f64, b: i32) -> f64 { return a + b; } fn main() { }", + "+", + ); + } + + #[test] + fn shift_result_is_lhs_type() { + ok("fn f(n: u32) -> u32 { return n << 2; } fn main() { }"); + } + + #[test] + fn shift_rhs_can_be_different_int() { + ok("fn f(n: u32, s: u8) -> u32 { return n >> s; } fn main() { }"); + } + + #[test] + fn shift_non_integer_lhs_is_error() { + err_contains( + "fn f(x: f64) -> f64 { return x << 1; } fn main() { }", + "integer LHS", + ); + } + + // ── Comparison / logical ────────────────────────────────────────────────── + + #[test] + fn comparison_ok() { + ok("fn f(a: i32, b: i32) -> bool { return a < b; } fn main() { }"); + } + + #[test] + fn logical_ok() { + ok("fn f(a: bool, b: bool) -> bool { return a and b; } fn main() { }"); + } + + #[test] + fn logical_non_bool_is_error() { + err_contains( + "fn f(a: i32, b: bool) -> bool { return a and b; } fn main() { }", + "`and`", + ); + } + + // ── Unary operators ─────────────────────────────────────────────────────── + + #[test] + fn neg_on_signed_ok() { + ok("fn f(x: i32) -> i32 { return -x; } fn main() { }"); + } + + #[test] + fn neg_on_unsigned_is_error() { + err_contains( + "fn f(x: u32) -> u32 { return -x; } fn main() { }", + "unary `-`", + ); + } + + #[test] + fn not_on_bool_ok() { + ok("fn f(b: bool) -> bool { return !b; } fn main() { }"); + } + + #[test] + fn not_on_int_is_error() { + err_contains( + "fn f(x: i32) -> bool { return !x; } fn main() { }", + "unary `!`", + ); + } + + #[test] + fn bitnot_on_integer_ok() { + ok("fn f(x: u32) -> u32 { return ~x; } fn main() { }"); + } + + // ── Pointers ───────────────────────────────────────────────────────────── + + #[test] + fn addrof_immutable_binding_gives_immutable_ptr() { + // taking address of immutable x gives *i32; assigning to *mut i32 should fail + err_contains( + "fn main() { let x: i32 = 1; let p: *mut i32 = &x; }", + "type mismatch", + ); + } + + #[test] + fn addrof_mutable_binding_gives_mut_ptr() { + ok("fn main() { let mut x: i32 = 1; let p: *mut i32 = &x; }"); + } + + #[test] + fn mut_ptr_coerces_to_immutable_ptr() { + ok("fn main() { let mut x: i32 = 1; let p: *i32 = &x; }"); + } + + #[test] + fn deref_typed_ptr_ok() { + ok("fn f(p: *i32) -> i32 { return *p; } fn main() { }"); + } + + #[test] + fn deref_opaque_ptr_is_error() { + err_contains( + "fn f(p: *opaque) { let x = *p; } fn main() { }", + "opaque pointer", + ); + } + + #[test] + fn addrof_non_place_is_error() { + err_contains("fn main() { let p = &(1 + 2); }", "non-place"); + } + + // ── Struct literals ─────────────────────────────────────────────────────── + + #[test] + fn struct_literal_ok() { + ok("struct Point { x: i32, y: i32 } + fn main() { let p: Point = Point { x: 1, y: 2 }; }"); + } + + #[test] + fn struct_literal_missing_field_is_error() { + err_contains( + "struct Point { x: i32, y: i32 } + fn main() { let p = Point { x: 1 }; }", + "missing field `y`", + ); + } + + #[test] + fn struct_literal_unknown_field_is_error() { + err_contains( + "struct Point { x: i32, y: i32 } + fn main() { let p = Point { x: 1, y: 2, z: 3 }; }", + "no field `z`", + ); + } + + #[test] + fn struct_literal_wrong_field_type_is_error() { + err_contains( + "struct S { x: bool } + fn main() { let s = S { x: 42 }; }", + "field `x`", + ); + } + + #[test] + fn struct_literal_undefined_struct_is_error() { + err_contains( + "fn main() { let s = Nope { x: 1 }; }", + "undefined struct `Nope`", + ); + } + + // ── Field access ───────────────────────────────────────────────────────── + + #[test] + fn field_access_ok() { + ok("struct S { v: i32 } + fn f(s: S) -> i32 { return s.v; } fn main() { }"); + } + + #[test] + fn field_access_unknown_field_is_error() { + err_contains( + "struct S { v: i32 } + fn f(s: S) -> i32 { return s.nope; } fn main() { }", + "no field `nope`", + ); + } + + #[test] + fn field_access_on_non_struct_is_error() { + err_contains( + "fn f(x: i32) -> i32 { return x.foo; } fn main() { }", + "non-struct", + ); + } + + // ── Index expressions ───────────────────────────────────────────────────── + + #[test] + fn array_index_ok() { + ok("fn f(arr: [i32; 4]) -> i32 { return arr[0]; } fn main() { }"); + } + + #[test] + fn pointer_index_ok() { + ok("fn f(p: *i32) -> i32 { return p[0]; } fn main() { }"); + } + + #[test] + fn non_integer_index_is_error() { + err_contains( + "fn f(arr: [i32; 4]) -> i32 { return arr[true]; } fn main() { }", + "index must be an integer", + ); + } + + // ── Function calls ──────────────────────────────────────────────────────── + + #[test] + fn call_wrong_arg_count_is_error() { + err_contains( + "fn add(a: i32, b: i32) -> i32 { return a + b; } + fn main() { let x = add(1); }", + "expects 2 argument(s), got 1", + ); + } + + #[test] + fn call_wrong_arg_type_is_error() { + err_contains( + "fn f(x: i32) { } + fn main() { f(true); }", + "argument 1", + ); + } + + #[test] + fn call_with_promotion_ok() { + // Passing i32 where i64 expected — implicit widening + ok("fn f(x: i64) { } + fn main() { let v: i32 = 1; f(v); }"); + } + + // ── Return type checking ────────────────────────────────────────────────── + + #[test] + fn missing_return_in_non_unit_fn_is_error() { + err_contains( + "fn f() -> i32 { let x = 1; } fn main() { }", + "must always return", + ); + } + + #[test] + fn return_wrong_type_is_error() { + err_contains( + "fn f() -> i32 { return true; } fn main() { }", + "type mismatch", + ); + } + + #[test] + fn return_value_from_unit_fn_is_error() { + err_contains("fn f() { return 1; } fn main() { }", "returns `()`"); + } + + #[test] + fn early_return_satisfies_all_paths() { + ok("fn f(b: bool) -> i32 { + if b { return 1; } else { return 2; } + } fn main() { }"); + } + + // ── Assignment / mutation ───────────────────────────────────────────────── + + #[test] + fn assign_to_immutable_is_error() { + err_contains("fn main() { let x: i32 = 0; x = 1; }", "mutable place"); + } + + #[test] + fn assign_to_mutable_ok() { + ok("fn main() { let mut x: i32 = 0; x = 1; }"); + } + + #[test] + fn compound_assign_ok() { + ok("fn main() { let mut x: i32 = 1; x += 2; }"); + } + + #[test] + fn compound_assign_immutable_is_error() { + err_contains("fn main() { let x: i32 = 1; x += 2; }", "mutable place"); + } + + // ── break / continue ───────────────────────────────────────────────────── + + #[test] + fn break_in_loop_ok() { + ok("fn main() { loop { break; } }"); + } + + #[test] + fn break_outside_loop_is_error() { + err_contains("fn main() { break; }", "outside of a loop"); + } + + #[test] + fn continue_in_while_ok() { + ok("fn main() { let mut i: i32 = 0; while i < 10 { i += 1; continue; } }"); + } + + #[test] + fn continue_outside_loop_is_error() { + err_contains("fn main() { continue; }", "outside of a loop"); + } + + // ── built-in string_view ────────────────────────────────────────────────── + + #[test] + fn string_view_fields_accessible() { + ok("fn f(s: string_view) -> u64 { return s.size; } fn main() { }"); + } + + #[test] + fn string_view_data_is_ptr_char() { + ok("fn f(s: string_view) -> *char { return s.data; } fn main() { }"); + } +} diff --git a/fluxc/src/parser.rs b/fluxc/src/parser.rs index 4e2aa9d..49e36d4 100644 --- a/fluxc/src/parser.rs +++ b/fluxc/src/parser.rs @@ -1,8 +1,8 @@ use crate::{ ast::{ BinaryOp, Block, CompoundAssignOp, ElseBranch, Expr, ExprKind, FieldDef, FuncDef, Param, - Program, Stmt, StmtKind, StructDef, StructField, TopLevelDef, TopLevelDefKind, Type, - UnaryOp, + Parsed, Program, Stmt, StmtKind, StructDef, StructField, TopLevelDef, TopLevelDefKind, + Type, UnaryOp, }, diagnostics::{Diagnostic, Label}, lexer::Lexer, @@ -235,7 +235,10 @@ impl<'src> Parser<'src> { self.advance(); Type::OpaquePointer { mutable } } else { - Type::Pointer { mutable, pointee: Box::new(self.parse_type()) } + Type::Pointer { + mutable, + pointee: Box::new(self.parse_type()), + } } } @@ -263,7 +266,7 @@ impl<'src> Parser<'src> { } /// Parse a block: `{ stmt* }`. - pub fn parse_block(&mut self) -> Block { + pub fn parse_block(&mut self) -> Block { let open = self.expect(TokenKind::LCurly); let mut stmts = Vec::new(); loop { @@ -288,7 +291,7 @@ impl<'src> Parser<'src> { /// - *Synchronization*: tokens that can never start a statement or /// expression trigger `synchronize()`, which skips forward until the /// next statement boundary to prevent cascading errors. - pub fn parse_stmt(&mut self) -> Stmt { + pub fn parse_stmt(&mut self) -> Stmt { let tok = self.current(); match tok.kind { TokenKind::Let => self.parse_let_stmt(), @@ -351,13 +354,13 @@ impl<'src> Parser<'src> { /// `allow_struct_literals` controls whether a bare `Ident { … }` is /// parsed as a struct literal. Pass `false` in `if`/`while` conditions /// so that `{` is not consumed as a struct body. - pub fn parse_expr(&mut self, allow_struct_literals: bool) -> Expr { + pub fn parse_expr(&mut self, allow_struct_literals: bool) -> Expr { self.pratt(0, allow_struct_literals) } // ── Statement helpers ───────────────────────────────────────────────────── - fn parse_let_stmt(&mut self) -> Stmt { + fn parse_let_stmt(&mut self) -> Stmt { let start = self.advance(); // consume `let` let mutable = if self.current().kind == TokenKind::Mut { self.advance(); @@ -391,7 +394,7 @@ impl<'src> Parser<'src> { } } - fn parse_return_stmt(&mut self) -> Stmt { + fn parse_return_stmt(&mut self) -> Stmt { let kw = self.advance(); // consume `return` // LL(1): `;` → unit return; anything else → parse expression let value = if self.current().kind != TokenKind::Semicolon { @@ -406,7 +409,7 @@ impl<'src> Parser<'src> { } } - fn parse_if_stmt(&mut self) -> Stmt { + fn parse_if_stmt(&mut self) -> Stmt { let kw = self.advance(); // consume `if` // Condition: expr_ns (no struct literals at outermost level) let cond = self.parse_expr(false); @@ -437,7 +440,7 @@ impl<'src> Parser<'src> { } } - fn parse_while_stmt(&mut self) -> Stmt { + fn parse_while_stmt(&mut self) -> Stmt { let kw = self.advance(); // consume `while` let cond = self.parse_expr(false); // no struct literals in condition let body = self.parse_block(); @@ -448,7 +451,7 @@ impl<'src> Parser<'src> { } } - fn parse_loop_stmt(&mut self) -> Stmt { + fn parse_loop_stmt(&mut self) -> Stmt { let kw = self.advance(); // consume `loop` let body = self.parse_block(); let span = kw.span.cover(body.span); @@ -458,7 +461,7 @@ impl<'src> Parser<'src> { } } - fn parse_expr_stmt(&mut self) -> Stmt { + fn parse_expr_stmt(&mut self) -> Stmt { let expr = self.parse_expr(true); let semi = self.expect(TokenKind::Semicolon); let span = expr.span.cover(semi.span); @@ -470,7 +473,7 @@ impl<'src> Parser<'src> { // ── Pratt core ──────────────────────────────────────────────────────────── - fn pratt(&mut self, min_bp: u8, allow_struct_lit: bool) -> Expr { + fn pratt(&mut self, min_bp: u8, allow_struct_lit: bool) -> Expr { let mut lhs = self.parse_nud(allow_struct_lit); loop { @@ -504,7 +507,7 @@ impl<'src> Parser<'src> { // ── Null denotation (prefix / primary) ─────────────────────────────────── - fn parse_nud(&mut self, allow_struct_lit: bool) -> Expr { + fn parse_nud(&mut self, allow_struct_lit: bool) -> Expr { let tok = self.advance(); match tok.kind { // Literals @@ -558,11 +561,11 @@ impl<'src> Parser<'src> { fn parse_led( &mut self, - lhs: Expr, + lhs: Expr, op_tok: Token<'src>, r_bp: u8, allow_struct_lit: bool, - ) -> Expr { + ) -> Expr { // Consume the operator token. self.advance(); @@ -647,7 +650,7 @@ impl<'src> Parser<'src> { /// Called after we have already parsed the leading `Ident` as `lhs` and /// the current token is `{`. - fn parse_struct_lit(&mut self, name_expr: Expr) -> Expr { + fn parse_struct_lit(&mut self, name_expr: Expr) -> Expr { let (name, name_span) = match name_expr.kind { ExprKind::Ident(ref s) => (s.clone(), name_expr.span), _ => unreachable!(), @@ -669,7 +672,7 @@ impl<'src> Parser<'src> { ) } - fn parse_struct_field_list(&mut self) -> Vec { + fn parse_struct_field_list(&mut self) -> Vec> { let mut fields = Vec::new(); loop { if matches!(self.current().kind, TokenKind::RCurly | TokenKind::Eof) { @@ -685,7 +688,7 @@ impl<'src> Parser<'src> { fields } - fn parse_struct_field(&mut self) -> StructField { + fn parse_struct_field(&mut self) -> StructField { let name_tok = self.expect(TokenKind::Ident); self.expect(TokenKind::Colon); // Struct literals allowed inside field values. @@ -702,7 +705,7 @@ impl<'src> Parser<'src> { // ── Top-level definitions ───────────────────────────────────────────────── /// Parse an entire source file as a `Program`. - pub fn parse_program(&mut self) -> Program { + pub fn parse_program(&mut self) -> Program { let start = self.current().span; let mut defs = Vec::new(); loop { @@ -716,7 +719,7 @@ impl<'src> Parser<'src> { } /// Parse one top-level definition (`fn` or `struct`). - pub fn parse_top_level_def(&mut self) -> TopLevelDef { + pub fn parse_top_level_def(&mut self) -> TopLevelDef { let tok = self.current(); match tok.kind { TokenKind::Fn => self.parse_func_def(), @@ -749,7 +752,7 @@ impl<'src> Parser<'src> { } } - fn parse_func_def(&mut self) -> TopLevelDef { + fn parse_func_def(&mut self) -> TopLevelDef { let kw = self.advance(); // consume `fn` let name_tok = self.expect(TokenKind::Ident); self.expect(TokenKind::LParen); @@ -809,7 +812,7 @@ impl<'src> Parser<'src> { } } - fn parse_struct_def(&mut self) -> TopLevelDef { + fn parse_struct_def(&mut self) -> TopLevelDef { let kw = self.advance(); // consume `struct` let name_tok = self.expect(TokenKind::Ident); self.expect(TokenKind::LCurly); @@ -855,7 +858,7 @@ impl<'src> Parser<'src> { /// Parse `arg, arg, …` up to `)`. The opening `(` has already been /// consumed by `parse_led`. Returns `(args, close_span)`. - fn parse_arg_list(&mut self) -> (Vec, Span) { + fn parse_arg_list(&mut self) -> (Vec>, Span) { let mut args = Vec::new(); loop { if matches!(self.current().kind, TokenKind::RParen | TokenKind::Eof) { @@ -879,21 +882,21 @@ impl<'src> Parser<'src> { #[cfg(test)] mod tests { use super::*; - use crate::ast::{ElseBranch, ExprKind, StmtKind, TopLevelDefKind, Type}; + use crate::ast::{ElseBranch, ExprKind, Parsed, StmtKind, TopLevelDefKind, Type}; // ── Expression test helpers ─────────────────────────────────────────────── - fn parse(src: &str) -> Expr { + fn parse(src: &str) -> Expr { Parser::new(src).parse_expr(true) } - fn parse_no_struct(src: &str) -> Expr { + fn parse_no_struct(src: &str) -> Expr { Parser::new(src).parse_expr(false) } // ── Statement test helpers ──────────────────────────────────────────────── - fn stmt(src: &str) -> Stmt { + fn stmt(src: &str) -> Stmt { Parser::new(src).parse_stmt() } @@ -1223,7 +1226,10 @@ mod tests { #[test] fn type_nested_pointer() { // `**i32` → Pointer { Pointer { I32 } } - assert!(matches!(parse_type_str("**i32"), Type::Pointer { mutable: false, .. })); + assert!(matches!( + parse_type_str("**i32"), + Type::Pointer { mutable: false, .. } + )); } #[test] @@ -1444,7 +1450,7 @@ mod tests { // ── Function definition tests ───────────────────────────────────────────── - fn top(src: &str) -> TopLevelDef { + fn top(src: &str) -> TopLevelDef { Parser::new(src).parse_top_level_def() } @@ -1503,7 +1509,10 @@ mod tests { let d = top("fn foo(p: *i32) { }"); match &d.kind { TopLevelDefKind::Func(f) => { - assert!(matches!(f.params[0].ty, Type::Pointer { mutable: false, .. })); + assert!(matches!( + f.params[0].ty, + Type::Pointer { mutable: false, .. } + )); } _ => panic!("expected func def"), } @@ -1543,7 +1552,10 @@ mod tests { let d = top("struct Node { value: i32, next: *Node }"); match &d.kind { TopLevelDefKind::Struct(s) => { - assert!(matches!(s.fields[1].ty, Type::Pointer { mutable: false, .. })); + assert!(matches!( + s.fields[1].ty, + Type::Pointer { mutable: false, .. } + )); } _ => panic!("expected struct def"), } @@ -1551,7 +1563,7 @@ mod tests { // ── Program tests ───────────────────────────────────────────────────────── - fn program(src: &str) -> Program { + fn program(src: &str) -> Program { Parser::new(src).parse_program() }