diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d73212b..d0faeb1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -126,6 +126,7 @@ pub enum BinaryOp { Subtract, Multiply, Divide, + Modulo, Equal, NotEqual, LessThan, diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 8c3aa0f..d96c1f5 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -542,6 +542,9 @@ fn emit_expr<'ctx>( BinaryOp::Divide => builder .build_int_signed_div(left, right, "divtmp") .map_err(|err| format!("failed to emit div instruction: {err}"))?, + BinaryOp::Modulo => builder + .build_int_signed_rem(left, right, "remtmp") + .map_err(|err| format!("failed to emit rem instruction: {err}"))?, BinaryOp::Equal | BinaryOp::NotEqual | BinaryOp::LessThan diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index 73995fc..afcdd34 100644 --- a/src/lexer/mod.rs +++ b/src/lexer/mod.rs @@ -12,6 +12,7 @@ pub enum Token { Minus, Star, Slash, + Percent, LeftParen, RightParen, LeftBrace, @@ -57,7 +58,36 @@ pub fn tokenize(source: &str) -> Result, String> { } '/' => { chars.next(); - tokens.push(Token::Slash); + match chars.peek() { + Some(&'/') => { + // Line comment: skip until end of line. + for next in chars.by_ref() { + if next == '\n' { + break; + } + } + } + Some(&'*') => { + chars.next(); + // Block comment: skip until the closing `*/`. + let mut terminated = false; + while let Some(next) = chars.next() { + if next == '*' && chars.peek() == Some(&'/') { + chars.next(); + terminated = true; + break; + } + } + if !terminated { + return Err("unterminated block comment".to_string()); + } + } + _ => tokens.push(Token::Slash), + } + } + '%' => { + chars.next(); + tokens.push(Token::Percent); } '(' => { chars.next(); @@ -187,7 +217,7 @@ pub fn tokenize(source: &str) -> Result, String> { } // TODO: Track line and column information for better lexer errors. -// TODO: Support comments and richer punctuation. +// TODO: Support richer punctuation (compound assignment, increment/decrement). #[cfg(test)] mod tests { @@ -290,6 +320,65 @@ mod tests { ); } + #[test] + fn skips_line_comments() { + let source = "int main() { // this is a comment\n return 5; // trailing\n}"; + let tokens = tokenize(source).expect("tokenization should succeed"); + + assert_eq!( + tokens, + vec![ + Token::Int, + Token::Identifier("main".to_string()), + Token::LeftParen, + Token::RightParen, + Token::LeftBrace, + Token::Return, + Token::Integer(5), + Token::Semicolon, + Token::RightBrace, + ] + ); + } + + #[test] + fn skips_block_comments() { + let source = "int main() { return /* the answer, \n spanning lines */ 5; }"; + let tokens = tokenize(source).expect("tokenization should succeed"); + + assert_eq!( + tokens, + vec![ + Token::Int, + Token::Identifier("main".to_string()), + Token::LeftParen, + Token::RightParen, + Token::LeftBrace, + Token::Return, + Token::Integer(5), + Token::Semicolon, + Token::RightBrace, + ] + ); + } + + #[test] + fn rejects_unterminated_block_comment() { + let error = tokenize("int main() { /* never closed").expect_err("should fail"); + + assert!(error.contains("unterminated block comment")); + } + + #[test] + fn tokenizes_percent_operator() { + let tokens = tokenize("7 % 3").expect("tokenization should succeed"); + + assert_eq!( + tokens, + vec![Token::Integer(7), Token::Percent, Token::Integer(3)] + ); + } + #[test] fn tokenizes_relational_and_logical_operators() { let source = "== != < <= > >= && || ! &"; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7db150c..4ababa6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -339,6 +339,7 @@ impl<'a> Parser<'a> { let operator = match self.peek() { Some(Token::Star) => BinaryOp::Multiply, Some(Token::Slash) => BinaryOp::Divide, + Some(Token::Percent) => BinaryOp::Modulo, _ => break, }; self.next(); diff --git a/tests/compiler_e2e.rs b/tests/compiler_e2e.rs index 64d541c..a3954a4 100644 --- a/tests/compiler_e2e.rs +++ b/tests/compiler_e2e.rs @@ -33,6 +33,25 @@ fn evaluates_division_and_subtraction() { assert_program_exit_code("int main() { return 20 / 5 - 1; }\n", 3); } +#[test] +fn evaluates_modulo() { + assert_program_exit_code("int main() { return 17 % 5; }\n", 2); +} + +#[test] +fn modulo_shares_multiplicative_precedence() { + // 10 % 4 * 2 groups left-to-right: (10 % 4) * 2 = 4, plus 1 = 5 + assert_program_exit_code("int main() { return 1 + 10 % 4 * 2; }\n", 5); +} + +#[test] +fn ignores_comments() { + assert_program_exit_code( + "// leading comment\nint main() {\n int x = 6; // set x\n /* block\n comment */\n return x % 4;\n}\n", + 2, + ); +} + #[test] fn evaluates_unary_negation() { // -5 modulo 256 is 251