From eb3663dfbb3ad62f2be638409c1b162b0ee1b25a Mon Sep 17 00:00:00 2001 From: Jooris Hadeler Date: Tue, 21 Apr 2026 10:56:42 +0200 Subject: [PATCH] feat: add support for booleans and comparision operators --- PLAN.md | 1 + e2e.sh | 45 ++++++++++++++++++++++++++ src/backend/cranelift.rs | 50 +++++++++++++++++++++++++++++ src/frontend/ast.rs | 8 +++++ src/frontend/lexer.rs | 22 +++++++++++++ src/frontend/parser.rs | 48 ++++++++++++++++++++++++++++ src/frontend/sema.rs | 68 ++++++++++++++++++++++++++++++++++++++-- src/frontend/token.rs | 18 +++++++++++ tests/booleans.c | 27 ++++++++++++++++ tests/booleans.src | 7 +++++ tests/return_42.src | 2 +- 11 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 tests/booleans.c create mode 100644 tests/booleans.src diff --git a/PLAN.md b/PLAN.md index 63ce27e..5839166 100644 --- a/PLAN.md +++ b/PLAN.md @@ -48,6 +48,7 @@ A Rust-flavored, C-targeting language - built pipeline-first. ## Planned Features (Backlog) ### Control flow +- [x] booleans and comparision operators - [ ] `if` / `else` branching - [ ] `while` loops diff --git a/e2e.sh b/e2e.sh index 301d57a..a508537 100755 --- a/e2e.sh +++ b/e2e.sh @@ -44,6 +44,48 @@ run_test() { echo } +# --- Helper Function --- +run_harness_test() { + local src_file="$1" + local harness_file="$2" + local expected_code="$3" + local base_name + base_name=$(basename "$src_file" .src) + local obj_file="tests/$base_name.o" + local exec_file="tests/$base_name" + + echo "--- Running test: $src_file ---" + + # 1. Compile the source file using our compiler + echo " [1/4] Compiling..." + cargo run --release -- "$src_file" -o "$obj_file" > /dev/null 2>&1 + + # 2. Link the object file with the system linker (gcc) + echo " [2/4] Linking with gcc..." + gcc "$obj_file" "$harness_file" -o "$exec_file" + + # 3. Run the executable + echo " [3/4] Running..." + set +e + ./"$exec_file" + local actual_code=$? + set -e + + # 4. Check the exit code + echo " [4/4] Verifying exit code..." + if [ "$actual_code" -eq "$expected_code" ]; then + echo "SUCCESS: Exit code is $actual_code as expected." + else + echo "FAILURE: Expected exit code $expected_code, but got $actual_code." + exit 1 + fi + + # Clean up generated files + rm "$obj_file" "$exec_file" + echo "------------------------------------" + echo +} + # --- Test Cases --- # Test a simple positive return value. @@ -53,4 +95,7 @@ run_test "tests/return_42.src" 42 # so -69 (i8) wraps around to 187. run_test "tests/return_neg_69.src" 187 +# Test boolean operations. +run_harness_test "tests/booleans.src" "tests/booleans.c" 0 + echo "All end-to-end tests passed!" \ No newline at end of file diff --git a/src/backend/cranelift.rs b/src/backend/cranelift.rs index 94476d0..3534426 100644 --- a/src/backend/cranelift.rs +++ b/src/backend/cranelift.rs @@ -192,6 +192,14 @@ impl<'a> FunctionTranslator<'a> { let inner_val = self.translate_expr(inner); match op { UnaryOp::Neg => self.builder.ins().ineg(inner_val), + UnaryOp::Not => { + // `!x` is equivalent to `x == 0` for booleans (0 or 1). + let ty = CraneliftBackend::lower_type(&inner.ty); + let zero = self.builder.ins().iconst(ty, 0); + self.builder + .ins() + .icmp(ir::condcodes::IntCC::Equal, inner_val, zero) + } } } TypedExprKind::Binary { op, lhs, rhs } => { @@ -218,6 +226,48 @@ impl<'a> FunctionTranslator<'a> { self.builder.ins().urem(lhs_val, rhs_val) } } + BinaryOp::Eq => { + self.builder + .ins() + .icmp(ir::condcodes::IntCC::Equal, lhs_val, rhs_val) + } + BinaryOp::Neq => { + self.builder + .ins() + .icmp(ir::condcodes::IntCC::NotEqual, lhs_val, rhs_val) + } + BinaryOp::Lt => { + let cc = if is_signed { + ir::condcodes::IntCC::SignedLessThan + } else { + ir::condcodes::IntCC::UnsignedLessThan + }; + self.builder.ins().icmp(cc, lhs_val, rhs_val) + } + BinaryOp::Le => { + let cc = if is_signed { + ir::condcodes::IntCC::SignedLessThanOrEqual + } else { + ir::condcodes::IntCC::UnsignedLessThanOrEqual + }; + self.builder.ins().icmp(cc, lhs_val, rhs_val) + } + BinaryOp::Gt => { + let cc = if is_signed { + ir::condcodes::IntCC::SignedGreaterThan + } else { + ir::condcodes::IntCC::UnsignedGreaterThan + }; + self.builder.ins().icmp(cc, lhs_val, rhs_val) + } + BinaryOp::Ge => { + let cc = if is_signed { + ir::condcodes::IntCC::SignedGreaterThanOrEqual + } else { + ir::condcodes::IntCC::UnsignedGreaterThanOrEqual + }; + self.builder.ins().icmp(cc, lhs_val, rhs_val) + } } } } diff --git a/src/frontend/ast.rs b/src/frontend/ast.rs index 3c1d664..78bb7cd 100644 --- a/src/frontend/ast.rs +++ b/src/frontend/ast.rs @@ -91,6 +91,7 @@ pub enum ExprKind { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UnaryOp { Neg, + Not, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -100,4 +101,11 @@ pub enum BinaryOp { Mul, Div, Rem, + + Eq, + Neq, + Lt, + Le, + Gt, + Ge, } diff --git a/src/frontend/lexer.rs b/src/frontend/lexer.rs index 9f0ea69..22e0b0c 100644 --- a/src/frontend/lexer.rs +++ b/src/frontend/lexer.rs @@ -142,6 +142,11 @@ impl<'src> Lexer<'src> { '/' => token!(TokenKind::Slash), '%' => token!(TokenKind::Percent), + '!' => token!(TokenKind::Bang, '=' => TokenKind::Unequal), + '=' => token!(TokenKind::Assign, '=' => TokenKind::Equal), + '<' => token!(TokenKind::LessThan, '=' => TokenKind::LessEqual), + '>' => token!(TokenKind::GreaterThan, '=' => TokenKind::GreaterEqual), + '.' => token!(TokenKind::Dot), ',' => token!(TokenKind::Comma), ':' => token!(TokenKind::Colon), @@ -283,4 +288,21 @@ mod test { ] ) } + + #[test] + fn comparison_and_logical() { + assert_eq!( + tokenize("== != < <= > >= ! ="), + vec![ + Token::new(TokenKind::Equal, "==", Span::new(0, 2)), + Token::new(TokenKind::Unequal, "!=", Span::new(3, 5)), + Token::new(TokenKind::LessThan, "<", Span::new(6, 7)), + Token::new(TokenKind::LessEqual, "<=", Span::new(8, 10)), + Token::new(TokenKind::GreaterThan, ">", Span::new(11, 12)), + Token::new(TokenKind::GreaterEqual, ">=", Span::new(13, 15)), + Token::new(TokenKind::Bang, "!", Span::new(16, 17)), + Token::new(TokenKind::Assign, "=", Span::new(18, 19)), + ] + ) + } } diff --git a/src/frontend/parser.rs b/src/frontend/parser.rs index 0878fdf..d449e5b 100644 --- a/src/frontend/parser.rs +++ b/src/frontend/parser.rs @@ -481,6 +481,7 @@ impl<'src> Parser<'src> { fn prefix_operator(&self, op: TokenKind) -> Option<(UnaryOp, u8)> { match op { TokenKind::Minus => Some((UnaryOp::Neg, 30)), + TokenKind::Bang => Some((UnaryOp::Not, 30)), _ => None, } @@ -490,6 +491,13 @@ impl<'src> Parser<'src> { /// or `None` if the [TokenKind] is not a valid infix operator. fn infix_operator(&self, op: TokenKind) -> Option<(BinaryOp, u8, u8)> { match op { + TokenKind::Equal => Some((BinaryOp::Eq, 5, 6)), + TokenKind::Unequal => Some((BinaryOp::Neq, 5, 6)), + TokenKind::LessThan => Some((BinaryOp::Lt, 5, 6)), + TokenKind::LessEqual => Some((BinaryOp::Le, 5, 6)), + TokenKind::GreaterThan => Some((BinaryOp::Gt, 5, 6)), + TokenKind::GreaterEqual => Some((BinaryOp::Ge, 5, 6)), + TokenKind::Plus => Some((BinaryOp::Add, 10, 11)), TokenKind::Minus => Some((BinaryOp::Sub, 10, 11)), @@ -764,4 +772,44 @@ mod test { }) ) } + + #[test] + fn comparison_expr() { + assert_eq!( + parse("a >= 5;", Parser::parse_expr), + Success(Expr { + kind: ExprKind::Binary { + op: BinaryOp::Ge, + lhs: Box::new(Expr { + kind: ExprKind::Identifier { + name: "a".to_string() + }, + span: Span::new(0, 1) + }), + rhs: Box::new(Expr { + kind: ExprKind::Integer { value: 5 }, + span: Span::new(5, 6) + }) + }, + span: Span::new(0, 6) + }) + ); + } + + #[test] + fn logical_not_expr() { + assert_eq!( + parse("!true;", Parser::parse_expr), + Success(Expr { + kind: ExprKind::Unary { + op: UnaryOp::Not, + expr: Box::new(Expr { + kind: ExprKind::Boolean { value: true }, + span: Span::new(1, 5) + }) + }, + span: Span::new(0, 5) + }) + ); + } } diff --git a/src/frontend/sema.rs b/src/frontend/sema.rs index 3a8680e..8abcba2 100644 --- a/src/frontend/sema.rs +++ b/src/frontend/sema.rs @@ -348,6 +348,25 @@ impl Sema { } } + ExprKind::Unary { + op: UnaryOp::Not, + expr, + } => { + let typed_inner = self.analyze_expr(expr); + + if let Err(e) = self.unify(&Ty::Bool, &typed_inner.ty) { + self.errors.push(SemanticError::new(e, expr.span)); + } + + TypedExpr { + kind: TypedExprKind::Unary { + op: UnaryOp::Not, + expr: Box::new(typed_inner), + }, + ty: Ty::Bool, + } + } + ExprKind::Binary { op, lhs, rhs } => { let typed_lhs = self.analyze_expr(lhs); let typed_rhs = self.analyze_expr(rhs); @@ -356,8 +375,24 @@ impl Sema { self.errors.push(SemanticError::new(e, expr.span)); } - let result_ty = typed_lhs.ty.clone(); - self.deferred_binary.push((expr.span, result_ty.clone())); + let is_comparison = matches!( + op, + BinaryOp::Eq + | BinaryOp::Neq + | BinaryOp::Lt + | BinaryOp::Le + | BinaryOp::Gt + | BinaryOp::Ge + ); + + let result_ty = if is_comparison { + Ty::Bool + } else { + typed_lhs.ty.clone() + }; + + self.deferred_binary.push((expr.span, typed_lhs.ty.clone())); + TypedExpr { kind: TypedExprKind::Binary { op: *op, @@ -628,4 +663,33 @@ mod test { .contains("unary minus only works on integer types") })); } + + #[test] + fn valid_logical_not() { + let src = "fn test(a: bool) -> bool { return !a; }"; + assert!(analyze(src).is_ok()); + } + + #[test] + fn invalid_logical_not() { + let src = "fn test(a: i32) -> bool { return !a; }"; + let errors = analyze(src).unwrap_err(); + assert!(errors.iter().any(|e| e.message.contains("type mismatch"))); + } + + #[test] + fn valid_comparison() { + let src = "fn test(a: i32, b: i32) -> bool { return a <= b; }"; + assert!(analyze(src).is_ok()); + } + + #[test] + fn invalid_comparison() { + let src = "fn test(a: bool, b: bool) -> bool { return a == b; }"; + let errors = analyze(src).unwrap_err(); + assert!(errors.iter().any(|e| { + e.message + .contains("binary operators only work on integer types") + })); + } } diff --git a/src/frontend/token.rs b/src/frontend/token.rs index d2efc4b..3d43715 100644 --- a/src/frontend/token.rs +++ b/src/frontend/token.rs @@ -70,6 +70,16 @@ pub enum TokenKind { Slash, Percent, + Equal, + Unequal, + LessThan, + LessEqual, + GreaterThan, + GreaterEqual, + + Assign, + Bang, + // Punctuation Dot, Comma, @@ -109,6 +119,14 @@ impl Display for TokenKind { TokenKind::Star => "`*`", TokenKind::Slash => "`/`", TokenKind::Percent => "`%`", + TokenKind::Equal => "`==`", + TokenKind::Unequal => "`!=`", + TokenKind::LessThan => "`<`", + TokenKind::LessEqual => "`<=`", + TokenKind::GreaterThan => "`>`", + TokenKind::GreaterEqual => "`>=`", + TokenKind::Assign => "`=`", + TokenKind::Bang => "`!`", TokenKind::Dot => "`.`", TokenKind::Comma => "`,`", TokenKind::Colon => "`:`", diff --git a/tests/booleans.c b/tests/booleans.c new file mode 100644 index 0000000..54ee56d --- /dev/null +++ b/tests/booleans.c @@ -0,0 +1,27 @@ +#include + +extern bool eq(int a, int b); +extern bool neq(int a, int b); +extern bool lt(int a, int b); +extern bool lte(int a, int b); +extern bool gt(int a, int b); +extern bool gte(int a, int b); +extern bool not_bool(bool a); + +int main() { + if (!eq(5, 5)) return 1; + if (eq(5, 6)) return 2; + if (!neq(5, 6)) return 3; + if (neq(5, 5)) return 4; + if (!lt(4, 5)) return 5; + if (lt(5, 5)) return 6; + if (!lte(5, 5)) return 7; + if (lte(6, 5)) return 8; + if (!gt(6, 5)) return 9; + if (gt(5, 5)) return 10; + if (!gte(5, 5)) return 11; + if (gte(4, 5)) return 12; + if (!not_bool(false)) return 13; + if (not_bool(true)) return 14; + return 0; // Success +} \ No newline at end of file diff --git a/tests/booleans.src b/tests/booleans.src new file mode 100644 index 0000000..d3b41b1 --- /dev/null +++ b/tests/booleans.src @@ -0,0 +1,7 @@ +fn eq(a: i32, b: i32) -> bool { return a == b; } +fn neq(a: i32, b: i32) -> bool { return a != b; } +fn lt(a: i32, b: i32) -> bool { return a < b; } +fn lte(a: i32, b: i32) -> bool { return a <= b; } +fn gt(a: i32, b: i32) -> bool { return a > b; } +fn gte(a: i32, b: i32) -> bool { return a >= b; } +fn not_bool(a: bool) -> bool { return !a; } \ No newline at end of file diff --git a/tests/return_42.src b/tests/return_42.src index f356e2d..3f7494b 100644 --- a/tests/return_42.src +++ b/tests/return_42.src @@ -1,3 +1,3 @@ fn main() -> i32 { - return 42; + return 10 * 4 + 2; } \ No newline at end of file