Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
use flake
5 changes: 5 additions & 0 deletions src/interpreter/exec_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ pub fn exec_stmt(stmt: &CheckedStmt, env: &mut Environment<Value>) -> 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",
)),
}
}

Expand Down
20 changes: 17 additions & 3 deletions src/ir/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -151,6 +152,8 @@ pub enum Statement<Ty> {
},
/// Return statement: `return [expr]`.
Return(Option<Box<ExprD<Ty>>>),
/// Assertion: `assert expr ;`. Fails at runtime if expr evaluates to false.
Assert(Box<ExprD<Ty>>),
}

/// A typed parameter: (name, type).
Expand All @@ -165,10 +168,19 @@ pub struct FunDecl<Ty> {
pub body: Box<StatementD<Ty>>,
}

/// 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<Ty> {
pub name: String,
pub body: Box<StatementD<Ty>>,
}

/// 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<Ty> {
pub functions: Vec<FunDecl<Ty>>,
pub tests: Vec<TestDecl<Ty>>,
}

// Type synonyms for checked and unchecked phases.
Expand All @@ -178,5 +190,7 @@ pub type UncheckedStmt = StatementD<()>;
pub type CheckedStmt = StatementD<Type>;
pub type UncheckedFunDecl = FunDecl<()>;
pub type CheckedFunDecl = FunDecl<Type>;
pub type UncheckedTestDecl = TestDecl<()>;
pub type CheckedTestDecl = TestDecl<Type>;
pub type UncheckedProgram = Program<()>;
pub type CheckedProgram = Program<Type>;
4 changes: 3 additions & 1 deletion src/parser/identifiers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
89 changes: 73 additions & 16 deletions src/parser/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,90 @@
//! 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
//!
//! ## `many0` as the top-level combinator
//!
//! `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)
}
11 changes: 10 additions & 1 deletion src/parser/statements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -67,13 +67,22 @@ pub fn statement(input: &str) -> IResult<&str, UncheckedStmt> {
if_statement,
while_statement,
return_statement,
assert_statement,
decl_statement,
call_statement,
assignment,
)),
)(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)?;
Expand Down
67 changes: 53 additions & 14 deletions src/semantic/type_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<CheckedProgram, TypeError> {
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)));
}
}

Expand Down Expand Up @@ -117,7 +126,27 @@ pub fn type_check(program: &UncheckedProgram) -> Result<CheckedProgram, TypeErro
let checked = type_check_fun_decl(f, &mut env, &fn_snapshot)?;
functions.push(checked);
}
Ok(Program { functions })

let mut tests = Vec::new();
for t in &program.tests {
let checked = type_check_test_decl(t, &mut env, &fn_snapshot)?;
tests.push(checked);
}

Ok(Program { functions, tests })
}

fn type_check_test_decl(
t: &UncheckedTestDecl,
env: &mut Environment<Type>,
fn_snapshot: &HashMap<String, Type>,
) -> Result<CheckedTestDecl, TypeError> {
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(
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions tests/fixtures/test_framework.minic
Original file line number Diff line number Diff line change
@@ -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;
}
42 changes: 42 additions & 0 deletions tests/type_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}