Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ pub enum Statement {
If(IfStatement),
While(WhileStatement),
For(ForStatement),
Break,
Continue,
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
35 changes: 35 additions & 0 deletions src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ pub fn generate_ir(program: &Program) -> Result<String, String> {
return_bb,
&function_types,
&module,
None,
)?;
}

Expand Down Expand Up @@ -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,
Expand All @@ -603,6 +612,7 @@ fn emit_statement<'ctx>(
return_bb: inkwell::basic_block::BasicBlock<'ctx>,
function_types: &HashMap<String, Type>,
module: &inkwell::module::Module<'ctx>,
loop_ctx: Option<LoopContext<'ctx>>,
) -> Result<(), String> {
match statement {
Statement::Declare(decl) => {
Expand Down Expand Up @@ -713,6 +723,7 @@ fn emit_statement<'ctx>(
return_bb,
function_types,
module,
loop_ctx,
)?;
}
variables.pop();
Expand Down Expand Up @@ -761,6 +772,7 @@ fn emit_statement<'ctx>(
return_bb,
function_types,
module,
loop_ctx,
)?;
if builder
.get_insert_block()
Expand All @@ -784,6 +796,7 @@ fn emit_statement<'ctx>(
return_bb,
function_types,
module,
loop_ctx,
)?;
}
if builder
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -875,6 +892,7 @@ fn emit_statement<'ctx>(
return_bb,
function_types,
module,
loop_ctx,
)?;
}

Expand Down Expand Up @@ -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()
Expand All @@ -954,6 +976,7 @@ fn emit_statement<'ctx>(
return_bb,
function_types,
module,
loop_ctx,
)?;
}
builder
Expand All @@ -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(())
}
Expand Down
19 changes: 19 additions & 0 deletions src/lexer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pub enum Token {
Else,
While,
For,
Break,
Continue,
Identifier(String),
Plus,
Minus,
Expand Down Expand Up @@ -245,6 +247,8 @@ pub fn tokenize(source: &str) -> Result<Vec<Token>, String> {
"else" => Token::Else,
"while" => Token::While,
"for" => Token::For,
"break" => Token::Break,
"continue" => Token::Continue,
_ => Token::Identifier(ident),
};

Expand Down Expand Up @@ -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");
Expand Down
10 changes: 10 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?;
Expand Down
39 changes: 39 additions & 0 deletions tests/compiler_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading