diff --git a/src/ast/mod.rs b/src/ast/mod.rs index fdf8077..c1ec8d9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -32,6 +32,8 @@ pub enum Statement { If(IfStatement), While(WhileStatement), For(ForStatement), + Break, + Continue, } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 3da9140..8bf288c 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -118,6 +118,7 @@ pub fn generate_ir(program: &Program) -> Result { return_bb, &function_types, &module, + None, )?; } @@ -593,6 +594,14 @@ fn emit_expr<'ctx>( } } +#[allow(clippy::too_many_arguments)] +/// Branch targets for `continue` and `break` inside the innermost loop. +#[derive(Clone, Copy)] +struct LoopContext<'ctx> { + continue_bb: inkwell::basic_block::BasicBlock<'ctx>, + break_bb: inkwell::basic_block::BasicBlock<'ctx>, +} + #[allow(clippy::too_many_arguments)] fn emit_statement<'ctx>( context: &'ctx Context, @@ -603,6 +612,7 @@ fn emit_statement<'ctx>( return_bb: inkwell::basic_block::BasicBlock<'ctx>, function_types: &HashMap, module: &inkwell::module::Module<'ctx>, + loop_ctx: Option>, ) -> Result<(), String> { match statement { Statement::Declare(decl) => { @@ -713,6 +723,7 @@ fn emit_statement<'ctx>( return_bb, function_types, module, + loop_ctx, )?; } variables.pop(); @@ -761,6 +772,7 @@ fn emit_statement<'ctx>( return_bb, function_types, module, + loop_ctx, )?; if builder .get_insert_block() @@ -784,6 +796,7 @@ fn emit_statement<'ctx>( return_bb, function_types, module, + loop_ctx, )?; } if builder @@ -848,6 +861,10 @@ fn emit_statement<'ctx>( return_bb, function_types, module, + Some(LoopContext { + continue_bb: cond_bb, + break_bb: merge_bb, + }), )?; if builder .get_insert_block() @@ -875,6 +892,7 @@ fn emit_statement<'ctx>( return_bb, function_types, module, + loop_ctx, )?; } @@ -931,6 +949,10 @@ fn emit_statement<'ctx>( return_bb, function_types, module, + Some(LoopContext { + continue_bb: step_bb, + break_bb: merge_bb, + }), )?; if builder .get_insert_block() @@ -954,6 +976,7 @@ fn emit_statement<'ctx>( return_bb, function_types, module, + loop_ctx, )?; } builder @@ -965,6 +988,18 @@ fn emit_statement<'ctx>( variables.pop(); } + Statement::Break => { + let ctx = loop_ctx.ok_or("`break` used outside of a loop")?; + builder + .build_unconditional_branch(ctx.break_bb) + .map_err(|err| format!("failed to emit break branch: {err}"))?; + } + Statement::Continue => { + let ctx = loop_ctx.ok_or("`continue` used outside of a loop")?; + builder + .build_unconditional_branch(ctx.continue_bb) + .map_err(|err| format!("failed to emit continue branch: {err}"))?; + } } Ok(()) } diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index 78c1021..50769bb 100644 --- a/src/lexer/mod.rs +++ b/src/lexer/mod.rs @@ -7,6 +7,8 @@ pub enum Token { Else, While, For, + Break, + Continue, Identifier(String), Plus, Minus, @@ -245,6 +247,8 @@ pub fn tokenize(source: &str) -> Result, String> { "else" => Token::Else, "while" => Token::While, "for" => Token::For, + "break" => Token::Break, + "continue" => Token::Continue, _ => Token::Identifier(ident), }; @@ -426,6 +430,21 @@ mod tests { ); } + #[test] + fn tokenizes_break_and_continue_keywords() { + let tokens = tokenize("break; continue;").expect("tokenization should succeed"); + + assert_eq!( + tokens, + vec![ + Token::Break, + Token::Semicolon, + Token::Continue, + Token::Semicolon, + ] + ); + } + #[test] fn tokenizes_increment_and_decrement_operators() { let tokens = tokenize("++ -- + + - -").expect("tokenization should succeed"); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 81f5514..7d0f333 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -206,6 +206,16 @@ impl<'a> Parser<'a> { body: Box::new(body), })) } + Some(Token::Break) => { + self.next(); + self.expect_semicolon()?; + Ok(Statement::Break) + } + Some(Token::Continue) => { + self.next(); + self.expect_semicolon()?; + Ok(Statement::Continue) + } Some(_) => { let statement = self.parse_assignment_statement()?; self.expect_semicolon()?; diff --git a/tests/compiler_e2e.rs b/tests/compiler_e2e.rs index 2d243fb..abddf39 100644 --- a/tests/compiler_e2e.rs +++ b/tests/compiler_e2e.rs @@ -33,6 +33,45 @@ fn evaluates_division_and_subtraction() { assert_program_exit_code("int main() { return 20 / 5 - 1; }\n", 3); } +#[test] +fn evaluates_break_in_while_loop() { + assert_program_exit_code( + "int main() { int x = 0; while (1) { x++; if (x == 7) { break; } } return x; }\n", + 7, + ); +} + +#[test] +fn evaluates_continue_in_for_loop() { + assert_program_exit_code( + "int main() { int total = 0; for (int i = 0; i < 10; i++) { if (i % 2 == 0) { continue; } total += i; } return total; }\n", + 25, + ); +} + +#[test] +fn break_applies_to_innermost_loop() { + assert_program_exit_code( + "int main() { int count = 0; for (int i = 0; i < 3; i++) { while (1) { break; } count++; } return count; }\n", + 3, + ); +} + +#[test] +fn rejects_break_outside_loop() { + let test_dir = make_test_dir(); + let input_path = test_dir.join("input.c"); + fs::write(&input_path, "int main() { break; return 0; }\n").expect("should write input"); + let compile_output = Command::new(COMPILER_BIN) + .arg(&input_path) + .current_dir(&test_dir) + .output() + .expect("should invoke compiler"); + assert!(!compile_output.status.success()); + let stderr = String::from_utf8_lossy(&compile_output.stderr); + assert!(stderr.contains("`break` used outside of a loop")); +} + #[test] fn evaluates_increment_and_decrement_statements() { assert_program_exit_code(