diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/src/interpreter/exec_stmt.rs b/src/interpreter/exec_stmt.rs index ceeda2c..75265d1 100644 --- a/src/interpreter/exec_stmt.rs +++ b/src/interpreter/exec_stmt.rs @@ -126,6 +126,11 @@ pub fn exec_stmt(stmt: &CheckedStmt, env: &mut Environment) -> ExecResult eval_call(name, arg_vals?, env)?; Ok(None) } + + // --- Assert (runtime support pending — Project 8, Part 3) --- + Statement::Assert(_) => Err(RuntimeError::new( + "assert is not yet supported by the interpreter", + )), } } diff --git a/src/ir/ast.rs b/src/ir/ast.rs index 5f57b24..f4359a6 100644 --- a/src/ir/ast.rs +++ b/src/ir/ast.rs @@ -9,9 +9,10 @@ //! * [`Literal`] — a constant value written directly in source code. //! * [`Expr`] / [`ExprD`] — expressions (arithmetic, comparisons, calls, …). //! * [`Statement`] / [`StatementD`] — statements (declarations, assignments, -//! `if`, `while`, `return`, blocks). +//! `if`, `while`, `return`, `assert`, blocks). //! * [`FunDecl`] — a single function declaration with its body. -//! * [`Program`] — the top-level container: a list of function declarations. +//! * [`TestDecl`] — a named test block with a body. +//! * [`Program`] — the top-level container: function declarations and test blocks. //! //! Convenience type aliases pin the `Ty` parameter to either `()` or `Type`: //! `UncheckedExpr`, `CheckedExpr`, `UncheckedProgram`, `CheckedProgram`, etc. @@ -151,6 +152,8 @@ pub enum Statement { }, /// Return statement: `return [expr]`. Return(Option>>), + /// Assertion: `assert expr ;`. Fails at runtime if expr evaluates to false. + Assert(Box>), } /// A typed parameter: (name, type). @@ -165,10 +168,19 @@ pub struct FunDecl { pub body: Box>, } -/// A complete MiniC program: function declarations only. Execution starts at `main`. +/// A named test block: `test "name" { stmts }`. +#[derive(Debug, Clone, PartialEq)] +pub struct TestDecl { + pub name: String, + pub body: Box>, +} + +/// A complete MiniC program: function declarations and test blocks. +/// Execution starts at `main` (--run mode) or runs all tests (--test mode). #[derive(Debug, Clone, PartialEq)] pub struct Program { pub functions: Vec>, + pub tests: Vec>, } // Type synonyms for checked and unchecked phases. @@ -178,5 +190,7 @@ pub type UncheckedStmt = StatementD<()>; pub type CheckedStmt = StatementD; pub type UncheckedFunDecl = FunDecl<()>; pub type CheckedFunDecl = FunDecl; +pub type UncheckedTestDecl = TestDecl<()>; +pub type CheckedTestDecl = TestDecl; pub type UncheckedProgram = Program<()>; pub type CheckedProgram = Program; diff --git a/src/parser/identifiers.rs b/src/parser/identifiers.rs index f6bdecb..cd92ba2 100644 --- a/src/parser/identifiers.rs +++ b/src/parser/identifiers.rs @@ -28,7 +28,9 @@ use nom::{ }; /// Reserved words: boolean literals and type names. -const RESERVED: &[&str] = &["true", "false", "int", "float", "bool", "str", "void", "return"]; +const RESERVED: &[&str] = &[ + "true", "false", "int", "float", "bool", "str", "void", "return", "assert", "test", +]; /// Parse an identifier (variable name). /// Must start with letter or underscore; subsequent chars may be letter, digit, or underscore. diff --git a/src/parser/program.rs b/src/parser/program.rs index f91be58..87ce506 100644 --- a/src/parser/program.rs +++ b/src/parser/program.rs @@ -5,15 +5,12 @@ //! Exposes one public function: //! //! * [`program`] — parses a complete MiniC program as a sequence of zero or -//! more function declarations and returns an +//! more function declarations and test blocks, returning an //! [`UncheckedProgram`]. //! -//! A valid MiniC program contains **only** function declarations at the top -//! level — there are no top-level statements or variable declarations outside -//! of functions. This constraint is enforced here by the grammar: `program` -//! is defined as `many0(fun_decl)`, so any token that does not start a -//! function declaration causes the parse to stop. The type checker then -//! verifies that a `main` function exists. +//! A valid MiniC program contains only function declarations and `test` blocks +//! at the top level. The type checker verifies that a `main` function exists +//! (required in `--run` mode). //! //! # Design Decisions //! @@ -21,17 +18,77 @@ //! //! `nom`'s `many0` combinator repeatedly applies a parser until it fails, //! collecting results in a `Vec`. Using it here means the program parser -//! naturally handles empty programs (zero functions) and programs with any -//! number of functions with no extra branching logic. The existence of -//! `main` is a semantic constraint checked in the next pipeline stage, not -//! a syntactic one enforced here. +//! naturally handles empty programs and programs with any number of +//! top-level items with no extra branching logic. -use crate::ir::ast::{Program, UncheckedProgram}; +use crate::ir::ast::{Program, TestDecl, UncheckedProgram, UncheckedTestDecl}; use crate::parser::functions::fun_decl; -use nom::{combinator::map, multi::many0, IResult}; +use crate::parser::statements::statement; +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete::{char, multispace0}, + combinator::map, + multi::many0, + sequence::{delimited, preceded}, + IResult, +}; -/// Parse a complete MiniC program: zero or more function declarations. -/// Execution starts at the `main` function (validated by the type checker). +enum TopItem { + Fun(crate::ir::ast::UncheckedFunDecl), + Test(UncheckedTestDecl), +} + +fn test_decl(input: &str) -> IResult<&str, UncheckedTestDecl> { + let (rest, _) = preceded(multispace0, tag("test"))(input)?; + let (rest, name) = preceded( + multispace0, + delimited(char('"'), nom::bytes::complete::take_while(|c| c != '"'), char('"')), + )(rest)?; + let (rest, body) = preceded( + multispace0, + map( + delimited( + preceded(multispace0, char('{')), + many0(statement), + preceded(multispace0, char('}')), + ), + |seq| crate::ir::ast::StatementD { + stmt: crate::ir::ast::Statement::Block { seq }, + ty: (), + }, + ), + )(rest)?; + Ok(( + rest, + TestDecl { + name: name.to_string(), + body: Box::new(body), + }, + )) +} + +fn top_item(input: &str) -> IResult<&str, TopItem> { + preceded( + multispace0, + alt(( + map(test_decl, TopItem::Test), + map(fun_decl, TopItem::Fun), + )), + )(input) +} + +/// Parse a complete MiniC program: zero or more function declarations and test blocks. pub fn program(input: &str) -> IResult<&str, UncheckedProgram> { - map(many0(fun_decl), |functions| Program { functions })(input) + map(many0(top_item), |items| { + let mut functions = Vec::new(); + let mut tests = Vec::new(); + for item in items { + match item { + TopItem::Fun(f) => functions.push(f), + TopItem::Test(t) => tests.push(t), + } + } + Program { functions, tests } + })(input) } diff --git a/src/parser/statements.rs b/src/parser/statements.rs index 9dcfef5..735ce7d 100644 --- a/src/parser/statements.rs +++ b/src/parser/statements.rs @@ -58,7 +58,7 @@ fn wrap(s: Statement<()>) -> UncheckedStmt { StatementD { stmt: s, ty: () } } -/// Parse any statement: block | if | while | return | decl | call | assignment. +/// Parse any statement: block | if | while | return | assert | decl | call | assignment. pub fn statement(input: &str) -> IResult<&str, UncheckedStmt> { preceded( multispace0, @@ -67,6 +67,7 @@ pub fn statement(input: &str) -> IResult<&str, UncheckedStmt> { if_statement, while_statement, return_statement, + assert_statement, decl_statement, call_statement, assignment, @@ -74,6 +75,14 @@ pub fn statement(input: &str) -> IResult<&str, UncheckedStmt> { )(input) } +/// Parse an assert statement: `assert expr ;`. +fn assert_statement(input: &str) -> IResult<&str, UncheckedStmt> { + let (rest, _) = preceded(multispace0, tag("assert"))(input)?; + let (rest, expr) = preceded(multispace0, expression)(rest)?; + let (rest, _) = preceded(multispace0, char(';'))(rest)?; + Ok((rest, wrap(Statement::Assert(Box::new(expr))))) +} + /// Parse a return statement: `return [expr] ;`. fn return_statement(input: &str) -> IResult<&str, UncheckedStmt> { let (rest, _) = preceded(multispace0, tag("return"))(input)?; diff --git a/src/semantic/type_checker.rs b/src/semantic/type_checker.rs index 46681cf..2869a0b 100644 --- a/src/semantic/type_checker.rs +++ b/src/semantic/type_checker.rs @@ -48,9 +48,9 @@ use std::collections::HashMap; use crate::environment::Environment; use crate::ir::ast::{ - CheckedExpr, CheckedFunDecl, CheckedProgram, CheckedStmt, Expr, ExprD, FunDecl, Literal, - Program, Statement, StatementD, Type, UncheckedExpr, UncheckedFunDecl, UncheckedProgram, - UncheckedStmt, + CheckedExpr, CheckedFunDecl, CheckedProgram, CheckedStmt, CheckedTestDecl, Expr, ExprD, + FunDecl, Literal, Program, Statement, StatementD, TestDecl, Type, UncheckedExpr, + UncheckedFunDecl, UncheckedProgram, UncheckedStmt, UncheckedTestDecl, }; use crate::stdlib::NativeRegistry; @@ -79,16 +79,25 @@ impl std::error::Error for TypeError {} /// Type-check a program. Returns `Ok(CheckedProgram)` if well-typed, `Err(TypeError)` on first error. /// Requires a `main` function with signature `void main()`. pub fn type_check(program: &UncheckedProgram) -> Result { - let main_fn = program.functions.iter().find(|f| f.name == "main"); - match main_fn { - None => return Err(TypeError::new("program must have a main function")), - Some(f) => { - if f.return_type != Type::Unit { - return Err(TypeError::new("main function must return void")); - } - if !f.params.is_empty() { - return Err(TypeError::new("main function must have no parameters")); - } + // In --test mode a program may have no main function, but if main exists it must be void(). + if let Some(f) = program.functions.iter().find(|f| f.name == "main") { + if f.return_type != Type::Unit { + return Err(TypeError::new("main function must return void")); + } + if !f.params.is_empty() { + return Err(TypeError::new("main function must have no parameters")); + } + } + // Require main when there are no test blocks (pure --run programs). + if program.tests.is_empty() && !program.functions.iter().any(|f| f.name == "main") { + return Err(TypeError::new("program must have a main function")); + } + + // Detect duplicate test names. + let mut seen_tests = std::collections::HashSet::new(); + for t in &program.tests { + if !seen_tests.insert(&t.name) { + return Err(TypeError::new(format!("duplicate test name: \"{}\"", t.name))); } } @@ -117,7 +126,27 @@ pub fn type_check(program: &UncheckedProgram) -> Result, + fn_snapshot: &HashMap, +) -> Result { + env.restore(fn_snapshot.clone()); + let body = type_check_stmt(&t.body, env, &Type::Unit)?; + Ok(TestDecl { + name: t.name.clone(), + body: Box::new(body), + }) } fn type_check_fun_decl( @@ -232,6 +261,16 @@ fn type_check_stmt( body: Box::new(body_checked), } } + Statement::Assert(expr) => { + let checked = type_check_expr_to_typed(expr, env)?; + if checked.ty != Type::Bool { + return Err(TypeError::new(format!( + "assert requires Bool, got {:?}", + checked.ty + ))); + } + Statement::Assert(Box::new(checked)) + } Statement::Return(expr) => match expr { None => { if *expected_return != Type::Unit { diff --git a/tests/fixtures/test_framework.minic b/tests/fixtures/test_framework.minic new file mode 100644 index 0000000..4e9a50f --- /dev/null +++ b/tests/fixtures/test_framework.minic @@ -0,0 +1,12 @@ +int add(int a, int b) { return a + b; } + +test "addition is correct" { + assert add(2, 3) == 5; + assert add(0, 0) == 0; +} + +test "comparison" { + int x = 10; + assert x > 5; + assert x == 10; +} diff --git a/tests/type_checker.rs b/tests/type_checker.rs index 3357161..ef17e05 100644 --- a/tests/type_checker.rs +++ b/tests/type_checker.rs @@ -202,3 +202,45 @@ fn test_type_check_print_wrong_arity() { let result = parse_and_type_check("void main() { print(1, 2); }"); assert!(result.is_err(), "expected arity error for print(1, 2)"); } + +// --------------------------------------------------------------------------- +// 8. Built-in test framework +// --------------------------------------------------------------------------- + +#[test] +fn test_type_check_assert_bool_ok() { + assert!(parse_and_type_check("void main() { assert true; }").is_ok()); +} + +#[test] +fn test_type_check_assert_non_bool_err() { + let result = parse_and_type_check("void main() { assert 42; }"); + assert!(result.is_err()); + assert!(result.unwrap_err().message.contains("Bool")); +} + +#[test] +fn test_type_check_test_block_ok() { + let result = parse_and_type_check(r#"test "t" { assert true; }"#); + assert!(result.is_ok()); +} + +#[test] +fn test_type_check_test_block_no_main_required() { + // A file with only test blocks (no main) should pass type checking. + let result = parse_and_type_check(r#"test "t" { assert 1 == 1; }"#); + assert!(result.is_ok(), "{}", result.unwrap_err().message); +} + +#[test] +fn test_type_check_duplicate_test_name_err() { + let result = parse_and_type_check(r#"test "foo" { assert true; } test "foo" { assert false; }"#); + assert!(result.is_err()); + assert!(result.unwrap_err().message.contains("duplicate")); +} + +#[test] +fn test_type_check_test_block_bad_assert_type() { + let result = parse_and_type_check(r#"test "t" { assert 1 + 1; }"#); + assert!(result.is_err()); +}