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
4 changes: 4 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
use flake

# Add scripts directory to PATH for convenient access to minic wrapper
PATH_add "$(pwd)/scripts"
2 changes: 2 additions & 0 deletions scripts/minic
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec "$(dirname "$0")/../target/debug/mini_c" "$@"
3 changes: 3 additions & 0 deletions src/interpreter/exec_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ pub fn exec_stmt(stmt: &CheckedStmt, env: &mut Environment<Value>) -> ExecResult
}
}
},

// --- Switch ---
Statement::Switch { .. } => todo!("switch statement interpreter not implemented yet"),

// --- Return ---
Statement::Return(Some(expr)) => {
Expand Down
7 changes: 6 additions & 1 deletion src/ir/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//! * [`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`, `switch`, `return`, blocks).
//! * [`FunDecl`] — a single function declaration with its body.
//! * [`Program`] — the top-level container: a list of function declarations.
//!
Expand Down Expand Up @@ -149,6 +149,11 @@ pub enum Statement<Ty> {
cond: Box<ExprD<Ty>>,
body: Box<StatementD<Ty>>,
},
Switch {
target: Box<ExprD<Ty>>,
cases: Vec<(Literal, Vec<StatementD<Ty>>)>,
default: Vec<StatementD<Ty>>,
},
/// Return statement: `return [expr]`.
Return(Option<Box<ExprD<Ty>>>),
}
Expand Down
58 changes: 52 additions & 6 deletions src/parser/statements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,24 @@
//! Exposes two public functions:
//!
//! * [`statement`] — the top-level entry point; tries each statement form in
//! order: `return`, `if`, `while`, call-statement, block, declaration,
//! order: `return`, `if`, `while`, `switch`, call-statement, block, declaration,
//! assignment.
//! * [`assignment`] — parses `lvalue = expression ;`; exported separately
//! because the test suite uses it directly.
//!
//! # Grammar
//!
//! ```text
//! statement := block | if_stmt | while_stmt | simple ';'
//! statement := block | if_stmt | while_stmt | switch_stmt | simple ';'
//! block := '{' statement* '}'
//! if_stmt := 'if' expr block ['else' block]
//! while_stmt := 'while' expr block
//! switch (expr) { case literal: statement+; … default: statement+ }
//! simple := return | decl | call | assign
//! ```
//!
//! Every simple statement is terminated by `;`.
//! Compound statements (`if`, `while`, block) end with `}` and need no `;`.
//! Compound statements (`if`, `while`, `switch`, block) end with `}` and need no `;`.
//!
//! # Design Decisions
//!
Expand All @@ -40,16 +41,17 @@
//! suffixes in a loop using the same pattern as the `primary` parser in
//! `expressions.rs`, producing a left-associative `Index` chain.

use crate::ir::ast::{Expr, ExprD, Statement, StatementD, UncheckedExpr, UncheckedStmt};
use crate::ir::ast::{Literal, Expr, ExprD, Statement, StatementD, UncheckedExpr, UncheckedStmt};
use crate::parser::expressions::{expression, parse_call};
use crate::parser::functions::type_name;
use crate::parser::identifiers::identifier;
use crate::parser::literals::{integer_literal, boolean_literal};
use nom::{
branch::alt,
bytes::complete::tag,
character::complete::{char, multispace0},
combinator::{map, opt},
multi::many0,
multi::{many0, many1},
sequence::{delimited, preceded, tuple},
IResult,
};
Expand All @@ -58,14 +60,15 @@ 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 | switch | return | decl | call | assignment.
pub fn statement(input: &str) -> IResult<&str, UncheckedStmt> {
preceded(
multispace0,
alt((
block_statement,
if_statement,
while_statement,
switch_statement,
return_statement,
decl_statement,
call_statement,
Expand Down Expand Up @@ -160,6 +163,49 @@ fn while_statement(input: &str) -> IResult<&str, UncheckedStmt> {
))
}

/// Parse a switch statement: `switch (expr) { case literal: statement+; … default: statement+ }`.
fn switch_statement(input: &str) -> IResult<&str, UncheckedStmt> {
let (rest, _) = preceded(multispace0, tag("switch"))(input)?;
let (rest, target) = preceded(multispace0, expression)(rest)?;
let (rest, _) = preceded(multispace0, char('{'))(rest)?;

let (rest, cases) = many1(map(
tuple((
preceded(multispace0, tag("case")),
preceded(
multispace0,
alt((
map(integer_literal, |i| Literal::Int(i)),
map(boolean_literal, |b| Literal::Bool(b))
))
),
preceded(multispace0, char(':')),
many1(preceded(multispace0, statement)),
)),
|(_, literal, _, statements)| (literal, statements),
))(rest)?;

let (rest, default) = preceded(multispace0, map(
tuple((
preceded(multispace0, tag("default")),
preceded(multispace0, char(':')),
many1(preceded(multispace0, statement)),
)),
|(_, _, statements)| statements,
))(rest)?;

let (rest, _) = preceded(multispace0, char('}'))(rest)?;

Ok((
rest,
wrap(Statement::Switch {
target: Box::new(target),
cases,
default,
}),
))
}

/// Parse an lvalue: identifier followed by zero or more `[ expr ]` suffixes.
fn lvalue(input: &str) -> IResult<&str, UncheckedExpr> {
let (mut rest, id) = preceded(multispace0, identifier)(input)?;
Expand Down
1 change: 1 addition & 0 deletions src/semantic/type_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ fn type_check_stmt(
body: Box::new(body_checked),
}
}
Statement::Switch { .. } => todo!("switch statement type checker not implemented yet"),
Statement::Return(expr) => match expr {
None => {
if *expected_return != Type::Unit {
Expand Down
218 changes: 218 additions & 0 deletions tests/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -672,3 +672,221 @@ fn test_array_in_expression() {
assert!(matches!(result.exp, Expr::Index { ref base, ref index }
if matches!(base.exp, Expr::ArrayLit(_)) && index.exp == Expr::Literal(Literal::Int(0))));
}

// --- Switch ---

#[test]
fn test_switch_statement() {
let result = statement("switch x { case 1: y = 1; default: y = 0; }").unwrap().1;
assert!(matches!(result.stmt, Statement::Switch { ref target, ref cases, ref default }
if matches!(target.exp, Expr::Ident(ref s) if s == "x") && cases.len() == 1 && default.len() == 1));
if let Statement::Switch { ref cases, ref default, .. } = result.stmt {
assert!(matches!(cases[0].0, Literal::Int(1)));
assert!(matches!(cases[0].1.len(), 1));
if let Statement::Assign { ref target, ref value } = cases[0].1[0].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "y"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(1)));
}
assert!(matches!(default.len(), 1));
if let Statement::Assign { ref target, ref value } = default[0].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "y"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(0)));
}
}
}

#[test]
fn test_switch_multiple_cases() {
let result = statement("switch x { case 1: y = 1; case 2: y = 2; default: y = 0; }").unwrap().1;
assert!(matches!(result.stmt, Statement::Switch { ref target, ref cases, ref default }
if matches!(target.exp, Expr::Ident(ref s) if s == "x") && cases.len() == 2 && default.len() == 1));
if let Statement::Switch { ref cases, ref default, .. } = result.stmt {
assert!(matches!(cases[0].0, Literal::Int(1)));
assert!(matches!(cases[0].1.len(), 1));
if let Statement::Assign { ref target, ref value } = cases[0].1[0].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "y"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(1)));
}
assert!(matches!(cases[1].0, Literal::Int(2)));
assert!(matches!(cases[1].1.len(), 1));
if let Statement::Assign { ref target, ref value } = cases[1].1[0].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "y"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(2)));
}
assert!(matches!(default.len(), 1));
if let Statement::Assign { ref target, ref value } = default[0].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "y"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(0)));
}
}
}

#[test]
fn test_switch_multiple_statements() {
let result = statement("switch x { case 1: y = 1; z = 2; default: y = 0; z = 1; }").unwrap().1;
assert!(matches!(result.stmt, Statement::Switch { ref target, ref cases, ref default }
if matches!(target.exp, Expr::Ident(ref s) if s == "x") && cases.len() == 1 && default.len() == 2));
if let Statement::Switch { ref cases, ref default, .. } = result.stmt {
assert!(matches!(cases[0].0, Literal::Int(1)));
assert!(matches!(cases[0].1.len(), 2));
if let Statement::Assign { ref target, ref value } = cases[0].1[0].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "y"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(1)));
}
if let Statement::Assign { ref target, ref value } = cases[0].1[1].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "z"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(2)));
}
assert!(matches!(default.len(), 2));
if let Statement::Assign { ref target, ref value } = default[0].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "y"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(0)));
}
if let Statement::Assign { ref target, ref value } = default[1].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "z"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(1)));
}
}
}

#[test]
fn test_switch_multiple_cases_and_statements() {
let result = statement("switch x { case 1: y = 1; z = 2; case 2: y = 2; z = 3; default: y = 0; z = 1; }").unwrap().1;
assert!(matches!(result.stmt, Statement::Switch { ref target, ref cases, ref default }
if matches!(target.exp, Expr::Ident(ref s) if s == "x") && cases.len() == 2 && default.len() == 2));
if let Statement::Switch { ref cases, ref default, .. } = result.stmt {
assert!(matches!(cases[0].0, Literal::Int(1)));
assert!(matches!(cases[0].1.len(), 2));
if let Statement::Assign { ref target, ref value } = cases[0].1[0].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "y"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(1)));
}
if let Statement::Assign { ref target, ref value } = cases[0].1[1].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "z"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(2)));
}
assert!(matches!(cases[1].0, Literal::Int(2)));
assert!(matches!(cases[1].1.len(), 2));
if let Statement::Assign { ref target, ref value } = cases[1].1[0].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "y"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(2)));
}
if let Statement::Assign { ref target, ref value } = cases[1].1[1].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "z"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(3)));
}
assert!(matches!(default.len(), 2));
if let Statement::Assign { ref target, ref value } = default[0].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "y"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(0)));
}
if let Statement::Assign { ref target, ref value } = default[1].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "z"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(1)));
}
}
}

#[test]
fn test_switch_boolean_cases() {
let result = statement("switch x { case true: y = 1; case false: y = 0; default: y = 3; }").unwrap().1;
assert!(matches!(result.stmt, Statement::Switch { ref target, ref cases, ref default }
if matches!(target.exp, Expr::Ident(ref s) if s == "x") && cases.len() == 2 && default.len() == 1));
if let Statement::Switch { ref cases, ref default, .. } = result.stmt {
assert!(matches!(cases[0].0, Literal::Bool(true)));
assert!(matches!(cases[0].1.len(), 1));
if let Statement::Assign { ref target, ref value } = cases[0].1[0].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "y"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(1)));
}
assert!(matches!(cases[1].0, Literal::Bool(false)));
assert!(matches!(cases[1].1.len(), 1));
if let Statement::Assign { ref target, ref value } = cases[1].1[0].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "y"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(0)));
}
assert!(matches!(default.len(), 1));
if let Statement::Assign { ref target, ref value } = default[0].stmt {
assert!(matches!(target.exp, Expr::Ident(ref s) if s == "y"));
assert_eq!(value.exp, Expr::Literal(Literal::Int(3)));
}
}
}

#[test]
fn test_switch_multiple_defaults_err() {
assert!(statement("switch x { case 1: y = 1; default: y = 0; default: z = 2; }").is_err());
}

#[test]
fn test_switch_no_default_err() {
assert!(statement("switch x { case 1: y = 1; }").is_err());
}

#[test]
fn test_switch_no_cases_err() {
assert!(statement("switch x { default: y = 0; }").is_err());
}

#[test]
fn test_switch_non_expression_err() {
assert!(statement("switch { case 1: y = 1; default: y = 0; }").is_err());
}

#[test]
fn test_switch_invalid_case_literal() {
assert!(statement("switch x { case y: y = 1; default: y = 0; }").is_err());
assert!(statement("switch x { case 1.5: y = 1; default: y = 0; }").is_err());
assert!(statement("switch x { case \"str\": y = 1; default: y = 0; }").is_err());
}

#[test]
fn test_switch_invalid_syntax() {
assert!(statement("switch x { case 1 y = 1; default: y = 0; }").is_err());
assert!(statement("switch x { case 1: y = 1; default y = 0; }").is_err());
assert!(statement("switch x { case 1: y = 1; default: y = 0 ").is_err());
assert!(statement("switch x case 1: y = 1; default: y = 0; }").is_err());
assert!(statement("switch x { case 1: y = 1; default: y = 0;").is_err());
}

#[test]
fn test_switch_whitespace() {
assert!(statement("switch x { case 1: y = 1; default: y = 0; }").is_ok());
assert!(statement("switch x { case 1 : y = 1 ; default : y = 0 ; }").is_ok());
}

#[test]
fn test_switch_in_function() {
let result = fun_decl("void foo() { switch x { case 1: y = 1; default: y = 0; } }").unwrap().1;
assert!(matches!(result.body.stmt, Statement::Block { ref seq } if seq.len() == 1));
if let Statement::Block { ref seq } = result.body.stmt {
assert!(matches!(seq[0].stmt, Statement::Switch { ref target, ref cases, ref default }
if matches!(target.exp, Expr::Ident(ref s) if s == "x") && cases.len() == 1 && default.len() == 1));
}
}

#[test]
fn test_switch_in_if() {
let result = statement("if x { switch y { case 1: z = 1; default: z = 0; } }").unwrap().1;
assert!(matches!(result.stmt, Statement::If { .. }));
if let Statement::If { ref then_branch, .. } = &result.stmt {
assert!(matches!(then_branch.stmt, Statement::Block { ref seq } if seq.len() == 1));
if let Statement::Block { ref seq } = &then_branch.stmt {
assert!(matches!(seq[0].stmt, Statement::Switch { ref target, ref cases, ref default }
if matches!(target.exp, Expr::Ident(ref s) if s == "y") && cases.len() == 1 && default.len() == 1));
}
}
}

#[test]
fn test_switch_in_while() {
let result = statement("while x { switch y { case 1: z = 1; default: z = 0; } }").unwrap().1;
assert!(matches!(result.stmt, Statement::While { .. }));
if let Statement::While { ref body, .. } = &result.stmt {
assert!(matches!(body.stmt, Statement::Block { ref seq } if seq.len() == 1));
if let Statement::Block { ref seq } = &body.stmt {
assert!(matches!(seq[0].stmt, Statement::Switch { ref target, ref cases, ref default }
if matches!(target.exp, Expr::Ident(ref s) if s == "y") && cases.len() == 1 && default.len() == 1));
}
}
}