feat: add while loops, break and continue statements

This commit is contained in:
2026-04-21 23:53:41 +02:00
parent 68ec14e541
commit 92eefb5cf1
10 changed files with 397 additions and 9 deletions
+16 -8
View File
@@ -45,29 +45,37 @@ A Rust-flavored, C-targeting language - built pipeline-first.
- [x] Output System V AMD64 ABI-compliant `.o` machine code files
- [x] End-to-end test: compile a simple `fn` → link via `gcc` → run → correct exit code
## Phase 5 - The Ray Tracer Milestone (Current)
To successfully write a simple ray tracer, we need continuous math, data structures, and I/O. The following path establishes these prerequisites:
- [ ] **Floating-Point Support:** Add `f32`/`f64` types, decimal literals, and Cranelift lowering for `fadd`, `fmul`, etc.
- [ ] **FFI & Interop:** Implement `extern fn` declarations to bind C standard library functions (like `putchar` or `printf`) for `.ppm` image output.
- [ ] **Type Casting:** Add the `as` operator to convert floating-point color bounds `[0.0, 1.0]` into integer byte formats `[0, 255]`.
- [ ] **Pointers:** Add pointer types (`*T`), address-of (`&`), and dereference (`*`) operators.
- [ ] **Structs:** Add `struct` definitions, initializers, and field access (`ray.origin.x`) to represent 3D vectors, rays, and spheres.
- [ ] **Arrays:** Add fixed-size arrays (`[T; N]`) or heap allocations for the scene and framebuffers.
## Planned Features (Backlog)
### Control flow
### Control flow & Variables
- [x] booleans and comparision operators
- [x] `if` / `else` branching
- [ ] `while` loops
- [x] `while` loops, `break`, `continue`
- [x] `let` bindings and variable assignments
### Types & memory
- [ ] Typed pointers (`*T`)
- [ ] Opaque pointers (`*void` / `*opaque`)
- [ ] Raw pointer arithmetic & dereference
- [ ] Fixed-size arrays (`[T; N]`)
- [ ] Raw pointer arithmetic
- [ ] Slices (`&[T]` / `[]T`)
### Composite types
- [ ] Structs & field access
- [ ] Enums (C-style tagged unions)
- [ ] Pattern matching (`match` / `switch`)
### Strings & interop
- [ ] String literals & `*u8` handling
- [ ] String literals
- [ ] Variadic functions (for `printf` interop)
- [ ] `extern` / FFI declarations
### Tooling & backend
- [ ] Proper register allocator
+6
View File
@@ -107,4 +107,10 @@ run_test "tests/let_stmt.src" 30
# Test assignments and multi-assignments
run_test "tests/assign.src" 42
# Test while loops
run_test "tests/fibonacci.src" 55
# Test break and continue inside while loops
run_test "tests/while_break_continue.src" 16
echo "All end-to-end tests passed!"
+6
View File
@@ -98,6 +98,10 @@ pub enum StmtKind<P: Phase = Untyped> {
then: Box<Stmt<P>>,
elze: Option<Box<Stmt<P>>>,
},
While {
condition: Expr<P>,
body: Box<Stmt<P>>,
},
Return {
value: Option<Expr<P>>,
},
@@ -110,6 +114,8 @@ pub enum StmtKind<P: Phase = Untyped> {
Expression {
expr: Expr<P>,
},
Break,
Continue,
}
#[derive(Debug, PartialEq, Eq)]
+7 -1
View File
@@ -68,6 +68,9 @@ impl<'src> Lexer<'src> {
"else" => TokenKind::Else,
"return" => TokenKind::Return,
"let" => TokenKind::Let,
"while" => TokenKind::While,
"break" => TokenKind::Break,
"continue" => TokenKind::Continue,
"i8" => TokenKind::I8,
"i16" => TokenKind::I16,
@@ -206,7 +209,7 @@ mod test {
#[test]
fn identifiers() {
assert_eq!(
tokenize("HELLO _hello _0@ fn if else return let"),
tokenize("HELLO _hello _0@ fn if else return let while break continue"),
vec![
Token::new(TokenKind::Identifier, "HELLO", Span::new(0, 5)),
Token::new(TokenKind::Identifier, "_hello", Span::new(6, 12)),
@@ -217,6 +220,9 @@ mod test {
Token::new(TokenKind::Else, "else", Span::new(23, 27)),
Token::new(TokenKind::Return, "return", Span::new(28, 34)),
Token::new(TokenKind::Let, "let", Span::new(35, 38)),
Token::new(TokenKind::While, "while", Span::new(39, 44)),
Token::new(TokenKind::Break, "break", Span::new(45, 50)),
Token::new(TokenKind::Continue, "continue", Span::new(51, 59)),
]
)
}
+107
View File
@@ -287,6 +287,10 @@ impl<'src> Parser<'src> {
/// | if_stmt
/// | return_stmt
/// | let_stmt
/// | while_stmt
/// | break_stmt
/// | continue_stmt
/// | expr_stmt
/// ;
/// ```
pub fn parse_stmt(&mut self) -> ParseResult<Stmt> {
@@ -295,8 +299,11 @@ impl<'src> Parser<'src> {
match peek_token.kind {
TokenKind::LBrace => self.parse_compound_stmt(),
TokenKind::If => self.parse_if_stmt(),
TokenKind::While => self.parse_while_stmt(),
TokenKind::Return => self.parse_return_stmt(),
TokenKind::Let => self.parse_let_stmt(),
TokenKind::Break => self.parse_break_stmt(),
TokenKind::Continue => self.parse_continue_stmt(),
_ => self.parse_expr_stmt(),
}
}
@@ -374,6 +381,26 @@ impl<'src> Parser<'src> {
})
}
/// Parses a while statement.
///
/// ```ebnf
/// while_stmt = "while" expr compound_stmt ;
/// ```
fn parse_while_stmt(&mut self) -> ParseResult<Stmt> {
let while_token = self.expect(TokenKind::While)?;
let condition = self.parse_expr()?;
let body = self.parse_compound_stmt()?;
let span = while_token.span.join(body.span);
Ok(Stmt {
kind: StmtKind::While {
condition,
body: Box::new(body),
},
span,
})
}
/// Parses an expression statement.
///
/// ```ebnf
@@ -454,6 +481,38 @@ impl<'src> Parser<'src> {
})
}
/// Parses a break statement.
///
/// ```ebnf
/// break_stmt = "break" ";" ;
/// ```
fn parse_break_stmt(&mut self) -> ParseResult<Stmt> {
let break_token = self.expect(TokenKind::Break)?;
let semi_token = self.expect(TokenKind::Semicolon)?;
let span = break_token.span.join(semi_token.span);
Ok(Stmt {
kind: StmtKind::Break,
span,
})
}
/// Parses a continue statement.
///
/// ```ebnf
/// continue_stmt = "continue" ";" ;
/// ```
fn parse_continue_stmt(&mut self) -> ParseResult<Stmt> {
let continue_token = self.expect(TokenKind::Continue)?;
let semi_token = self.expect(TokenKind::Semicolon)?;
let span = continue_token.span.join(semi_token.span);
Ok(Stmt {
kind: StmtKind::Continue,
span,
})
}
// ====== Pratt Parsing Implementation ======
/// Parses an expression.
@@ -910,6 +969,54 @@ mod test {
);
}
#[test]
fn while_stmt() {
assert_eq!(
parse("while true { break; }", Parser::parse_stmt),
Success(Stmt {
kind: StmtKind::While {
condition: Expr {
kind: ExprKind::Boolean { value: true },
ty: (),
span: Span::new(6, 10)
},
body: Box::new(Stmt {
kind: StmtKind::Compound {
inner: vec![Stmt {
kind: StmtKind::Break,
span: Span::new(13, 19)
}]
},
span: Span::new(11, 21)
}),
},
span: Span::new(0, 21)
})
)
}
#[test]
fn break_stmt() {
assert_eq!(
parse("break;", Parser::parse_stmt),
Success(Stmt {
kind: StmtKind::Break,
span: Span::new(0, 6)
})
);
}
#[test]
fn continue_stmt() {
assert_eq!(
parse("continue;", Parser::parse_stmt),
Success(Stmt {
kind: StmtKind::Continue,
span: Span::new(0, 9)
})
);
}
#[test]
fn assign_expr_stmt() {
assert_eq!(
+68
View File
@@ -78,6 +78,7 @@ pub struct Sema {
deferred_unary_neg: Vec<(Span, Ty, Ty, Option<u64>)>,
deferred_binary: Vec<(Span, Ty)>,
deferred_literals: Vec<(Span, Ty)>,
loop_depth: usize,
}
impl Sema {
@@ -91,6 +92,7 @@ impl Sema {
deferred_unary_neg: Vec::new(),
deferred_binary: Vec::new(),
deferred_literals: Vec::new(),
loop_depth: 0,
}
}
@@ -323,6 +325,47 @@ impl Sema {
span: stmt.span,
}
}
StmtKind::While { condition, body } => {
let typed_condition = self.analyze_expr(condition);
if let Err(err) = self.unify(&typed_condition.ty, &Ty::Bool) {
self.errors.push(SemanticError::new(err, condition.span));
}
self.loop_depth += 1;
let typed_body = self.analyze_stmt(body, expected_ret_ty);
self.loop_depth -= 1;
TypedStmt {
kind: TypedStmtKind::While {
condition: typed_condition,
body: Box::new(typed_body),
},
span: stmt.span,
}
}
StmtKind::Break => {
if self.loop_depth == 0 {
self.errors
.push(SemanticError::new("`break` outside of a loop", stmt.span));
}
TypedStmt {
kind: TypedStmtKind::Break,
span: stmt.span,
}
}
StmtKind::Continue => {
if self.loop_depth == 0 {
self.errors.push(SemanticError::new(
"`continue` outside of a loop",
stmt.span,
));
}
TypedStmt {
kind: TypedStmtKind::Continue,
span: stmt.span,
}
}
StmtKind::Return { value } => {
if let Some(expr) = value {
let typed_expr = self.analyze_expr(expr);
@@ -604,6 +647,14 @@ impl Sema {
elze: elze.map(|s| Box::new(self.apply_subst_stmt(*s))),
},
TypedStmtKind::While { condition, body } => TypedStmtKind::While {
condition: self.apply_subst_expr(condition),
body: Box::new(self.apply_subst_stmt(*body)),
},
TypedStmtKind::Break => TypedStmtKind::Break,
TypedStmtKind::Continue => TypedStmtKind::Continue,
TypedStmtKind::Return { value } => TypedStmtKind::Return {
value: value.map(|e| self.apply_subst_expr(e)),
},
@@ -933,4 +984,21 @@ mod test {
.any(|e| e.message.contains("invalid left-hand side of assignment"))
);
}
#[test]
fn valid_while() {
let src = "fn test() { while true { break; continue; } }";
assert!(analyze(src).is_ok());
}
#[test]
fn invalid_break() {
let src = "fn test() { break; }";
let errors = analyze(src).unwrap_err();
assert!(
errors
.iter()
.any(|e| e.message.contains("`break` outside of a loop"))
);
}
}
+6
View File
@@ -60,6 +60,9 @@ pub enum TokenKind {
Else,
Return,
Let,
While,
Break,
Continue,
// Types
I8,
@@ -117,6 +120,9 @@ impl Display for TokenKind {
TokenKind::Else => "`else`",
TokenKind::Return => "`return`",
TokenKind::Let => "`let`",
TokenKind::While => "`while`",
TokenKind::Break => "`break`",
TokenKind::Continue => "`continue`",
TokenKind::I8 => "`i8`",
TokenKind::I16 => "`i16`",
TokenKind::I32 => "`i32`",
+146
View File
@@ -85,6 +85,9 @@ struct FuncBuilder {
/// Scoped mapping from user-defined variable names to their corresponding `LocalId`.
scopes: Vec<HashMap<String, LocalId>>,
/// Stack of `(continue_target, break_target)` for nested loops
loop_stack: Vec<(BlockId, BlockId)>,
}
impl FuncBuilder {
@@ -100,6 +103,7 @@ impl FuncBuilder {
current_statements: Vec::new(),
next_block_id: 0,
scopes: vec![HashMap::new()],
loop_stack: Vec::new(),
}
}
@@ -249,6 +253,57 @@ impl FuncBuilder {
self.switch_to_block(merge_block);
}
TypedStmtKind::While { condition, body } => {
let cond_block = self.new_block();
let body_block = self.new_block();
let merge_block = self.new_block();
self.terminate(Terminator {
kind: TerminatorKind::Goto { target: cond_block },
span: stmt.span,
});
self.switch_to_block(cond_block);
let cond_op = self.lower_expr(condition);
self.terminate(Terminator {
kind: TerminatorKind::CondBranch {
cond: cond_op,
target_true: body_block,
target_false: merge_block,
},
span: condition.span,
});
self.switch_to_block(body_block);
self.loop_stack.push((cond_block, merge_block));
self.lower_stmt(body);
self.loop_stack.pop();
self.terminate(Terminator {
kind: TerminatorKind::Goto { target: cond_block },
span: body.span,
});
self.switch_to_block(merge_block);
}
TypedStmtKind::Break => {
if let Some(&(_, merge_block)) = self.loop_stack.last() {
self.terminate(Terminator {
kind: TerminatorKind::Goto {
target: merge_block,
},
span: stmt.span,
});
}
}
TypedStmtKind::Continue => {
if let Some(&(cond_block, _)) = self.loop_stack.last() {
self.terminate(Terminator {
kind: TerminatorKind::Goto { target: cond_block },
span: stmt.span,
});
}
}
TypedStmtKind::Return { value } => {
let val_op = value.as_ref().map(|v| self.lower_expr(v));
self.terminate(Terminator {
@@ -330,3 +385,94 @@ impl FuncBuilder {
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::frontend::parser::Parser;
use crate::frontend::sema::Sema;
/// Helper function to parse, analyze, and build a MIR module from source code.
fn build_mir(source: &str) -> MirModule {
let mut parser = Parser::new(source);
let module = parser.parse_module();
if let Some(errors) = parser.errors() {
panic!("Parse errors: {:?}", errors);
}
let mut sema = Sema::new();
let typed_module = sema.analyze_module(&module);
if let Some(errors) = sema.errors() {
panic!("Semantic errors: {:?}", errors);
}
MirBuilder::build(&typed_module)
}
#[test]
fn test_lower_while_loop() {
let mir = build_mir("fn main() { while true { } }");
let func = &mir.functions[0];
// Ensure exactly 4 basic blocks are generated
assert_eq!(func.blocks.len(), 4);
// Block 0: Entry -> Goto(1)
assert!(matches!(
func.blocks[0].terminator.kind,
TerminatorKind::Goto { target: BlockId(1) }
));
// Block 1: Cond -> CondBranch(true -> 2, false -> 3)
assert!(matches!(
func.blocks[1].terminator.kind,
TerminatorKind::CondBranch {
target_true: BlockId(2),
target_false: BlockId(3),
..
}
));
// Block 2: Body -> Goto(1)
assert!(matches!(
func.blocks[2].terminator.kind,
TerminatorKind::Goto { target: BlockId(1) }
));
// Block 3: Merge -> Return
assert!(matches!(
func.blocks[3].terminator.kind,
TerminatorKind::Return { value: None }
));
}
#[test]
fn test_lower_break_and_continue() {
let mir = build_mir("fn main() { while true { continue; break; } }");
let func = &mir.functions[0];
// The body block (BlockId(2)) hits `continue` first, meaning it terminates immediately with a Goto back to the condition block.
// The trailing `break` is correctly skipped by the builder as automatic dead code!
assert!(matches!(
func.blocks[2].terminator.kind,
TerminatorKind::Goto { target: BlockId(1) }
));
}
#[test]
fn test_lower_let_and_assign() {
let mir = build_mir("fn main() { let a = 5; a = 10; }");
let func = &mir.functions[0];
assert_eq!(func.locals.len(), 1); // Only 1 variable 'a' (No temporaries generated for literals)
assert_eq!(func.blocks.len(), 1); // No branches, so 1 block
let block = &func.blocks[0];
assert_eq!(block.statements.len(), 2); // Includes both the let initialization and the later assignment
assert!(matches!(
block.statements[0].kind,
StatementKind::Assign(LocalId(0), _)
));
assert!(matches!(
block.statements[1].kind,
StatementKind::Assign(LocalId(0), _)
));
}
}
+15
View File
@@ -0,0 +1,15 @@
fn main() -> i32 {
let n = 10;
let a = 0;
let b = 1;
let i = 0;
while i < n {
let temp = a + b;
a = b;
b = temp;
i = i + 1;
}
return a;
}
+20
View File
@@ -0,0 +1,20 @@
fn main() -> i32 {
let i = 0;
let sum = 0;
while i < 10 {
i = i + 1;
if i % 2 == 0 {
continue;
}
if i > 7 {
break;
}
sum = sum + i;
}
return sum;
}