diff --git a/PLAN.md b/PLAN.md index 80f1814..49a270d 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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 diff --git a/e2e.sh b/e2e.sh index efbdcde..e233634 100755 --- a/e2e.sh +++ b/e2e.sh @@ -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!" \ No newline at end of file diff --git a/src/frontend/ast.rs b/src/frontend/ast.rs index 256b20a..112982a 100644 --- a/src/frontend/ast.rs +++ b/src/frontend/ast.rs @@ -98,6 +98,10 @@ pub enum StmtKind { then: Box>, elze: Option>>, }, + While { + condition: Expr

, + body: Box>, + }, Return { value: Option>, }, @@ -110,6 +114,8 @@ pub enum StmtKind { Expression { expr: Expr

, }, + Break, + Continue, } #[derive(Debug, PartialEq, Eq)] diff --git a/src/frontend/lexer.rs b/src/frontend/lexer.rs index add1c71..b680abf 100644 --- a/src/frontend/lexer.rs +++ b/src/frontend/lexer.rs @@ -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)), ] ) } diff --git a/src/frontend/parser.rs b/src/frontend/parser.rs index fe4d18b..ef1c6f6 100644 --- a/src/frontend/parser.rs +++ b/src/frontend/parser.rs @@ -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 { @@ -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 { + 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 { + 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 { + 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!( diff --git a/src/frontend/sema.rs b/src/frontend/sema.rs index 5483814..73ad6c0 100644 --- a/src/frontend/sema.rs +++ b/src/frontend/sema.rs @@ -78,6 +78,7 @@ pub struct Sema { deferred_unary_neg: Vec<(Span, Ty, Ty, Option)>, 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")) + ); + } } diff --git a/src/frontend/token.rs b/src/frontend/token.rs index c3412ca..27c30c4 100644 --- a/src/frontend/token.rs +++ b/src/frontend/token.rs @@ -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`", diff --git a/src/middle/builder.rs b/src/middle/builder.rs index c11bdcc..ff74938 100644 --- a/src/middle/builder.rs +++ b/src/middle/builder.rs @@ -85,6 +85,9 @@ struct FuncBuilder { /// Scoped mapping from user-defined variable names to their corresponding `LocalId`. scopes: Vec>, + + /// 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), _) + )); + } +} diff --git a/tests/fibonacci.src b/tests/fibonacci.src new file mode 100644 index 0000000..7ef4e6c --- /dev/null +++ b/tests/fibonacci.src @@ -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; +} \ No newline at end of file diff --git a/tests/while_break_continue.src b/tests/while_break_continue.src new file mode 100644 index 0000000..cf48779 --- /dev/null +++ b/tests/while_break_continue.src @@ -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; +} \ No newline at end of file