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
10 changes: 10 additions & 0 deletions src/interpreter/exec_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ pub fn exec_stmt(stmt: &CheckedStmt, env: &mut Environment<Value>) -> ExecResult
}
Statement::Return(None) => Ok(Some(Value::Void)),

// --- Assert ---
Statement::Assert(expr) => match eval_expr(expr, env)? {
Value::Bool(true) => Ok(None),
Value::Bool(false) => Err(RuntimeError::new("assertion failed")),
v => Err(RuntimeError::new(format!(
"assert requires bool, got: {}",
v
))),
},

// --- Statement-level function call ---
Statement::Call { name, args } => {
let arg_vals: Result<Vec<Value>, RuntimeError> =
Expand Down
45 changes: 38 additions & 7 deletions src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,25 +61,56 @@ use crate::stdlib::NativeRegistry;
use eval_expr::eval_call;
use value::{FnValue, RuntimeError, Value};

/// Interpret a type-checked MiniC program, starting execution at `main`.
pub fn interpret(program: &CheckedProgram) -> Result<(), RuntimeError> {
fn build_env(program: &CheckedProgram) -> Environment<Value> {
let mut env = Environment::<Value>::new();

// Register native stdlib functions as Value::Fn(FnValue::Native) bindings.
let registry = NativeRegistry::default();
for (name, entry) in registry.iter() {
env.declare(name.clone(), Value::Fn(FnValue::Native(entry.func)));
}

// Register user-defined functions as Value::Fn(FnValue::UserDefined) bindings.
for fun in &program.functions {
env.declare(fun.name.clone(), Value::Fn(FnValue::UserDefined(fun.clone())));
}
env
}

/// Interpret a type-checked MiniC program, starting execution at `main`.
pub fn interpret(program: &CheckedProgram) -> Result<(), RuntimeError> {
let mut env = build_env(program);
if env.get("main").is_none() {
return Err(RuntimeError::new("no 'main' function found"));
}

eval_call("main", vec![], &mut env)?;
Ok(())
}

/// Run all test blocks in a program. Prints PASS/FAIL per test and a summary.
/// Returns `Ok(())` if every test passed, `Err` if any failed.
pub fn run_tests(program: &CheckedProgram) -> Result<(), RuntimeError> {
use exec_stmt::exec_stmt;

let mut passed = 0usize;
let mut failed = 0usize;

for test in &program.tests {
let mut env = build_env(program);
match exec_stmt(&test.body, &mut env) {
Ok(_) => {
println!("PASS {}", test.name);
passed += 1;
}
Err(e) => {
println!("FAIL {} — {}", test.name, e.message);
failed += 1;
}
}
}

let total = passed + failed;
println!("{} / {} tests passed", passed, total);

if failed > 0 {
Err(RuntimeError::new(format!("{} test(s) failed", failed)))
} else {
Ok(())
}
}
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>;
9 changes: 8 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::{env, fs, process};

use mini_c::{interpreter::interpret, parser::program, semantic::type_check};
use mini_c::{interpreter::{interpret, run_tests}, parser::program, semantic::type_check};

fn usage() -> ! {
eprintln!("Usage: minic --check <file.minic>");
eprintln!(" minic --run <file.minic>");
eprintln!(" minic --test <file.minic>");
process::exit(1);
}

Expand Down Expand Up @@ -58,6 +59,12 @@ fn main() {
process::exit(1);
}
}
"--test" => {
if let Err(e) = run_tests(&checked) {
eprintln!("{}", e);
process::exit(1);
}
}
// Task 1.1: unknown flag
_ => usage(),
}
Expand Down
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
Loading