feat: add support for booleans and comparision operators

This commit is contained in:
2026-04-21 10:56:42 +02:00
parent bad6b9e116
commit eb3663dfbb
11 changed files with 293 additions and 3 deletions
+1
View File
@@ -48,6 +48,7 @@ A Rust-flavored, C-targeting language - built pipeline-first.
## Planned Features (Backlog) ## Planned Features (Backlog)
### Control flow ### Control flow
- [x] booleans and comparision operators
- [ ] `if` / `else` branching - [ ] `if` / `else` branching
- [ ] `while` loops - [ ] `while` loops
+45
View File
@@ -44,6 +44,48 @@ run_test() {
echo 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 Cases ---
# Test a simple positive return value. # Test a simple positive return value.
@@ -53,4 +95,7 @@ run_test "tests/return_42.src" 42
# so -69 (i8) wraps around to 187. # so -69 (i8) wraps around to 187.
run_test "tests/return_neg_69.src" 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!" echo "All end-to-end tests passed!"
+50
View File
@@ -192,6 +192,14 @@ impl<'a> FunctionTranslator<'a> {
let inner_val = self.translate_expr(inner); let inner_val = self.translate_expr(inner);
match op { match op {
UnaryOp::Neg => self.builder.ins().ineg(inner_val), 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 } => { TypedExprKind::Binary { op, lhs, rhs } => {
@@ -218,6 +226,48 @@ impl<'a> FunctionTranslator<'a> {
self.builder.ins().urem(lhs_val, rhs_val) 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)
}
} }
} }
} }
+8
View File
@@ -91,6 +91,7 @@ pub enum ExprKind {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnaryOp { pub enum UnaryOp {
Neg, Neg,
Not,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -100,4 +101,11 @@ pub enum BinaryOp {
Mul, Mul,
Div, Div,
Rem, Rem,
Eq,
Neq,
Lt,
Le,
Gt,
Ge,
} }
+22
View File
@@ -142,6 +142,11 @@ impl<'src> Lexer<'src> {
'/' => token!(TokenKind::Slash), '/' => token!(TokenKind::Slash),
'%' => token!(TokenKind::Percent), '%' => 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::Dot),
',' => token!(TokenKind::Comma), ',' => token!(TokenKind::Comma),
':' => token!(TokenKind::Colon), ':' => 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)),
]
)
}
} }
+48
View File
@@ -481,6 +481,7 @@ impl<'src> Parser<'src> {
fn prefix_operator(&self, op: TokenKind) -> Option<(UnaryOp, u8)> { fn prefix_operator(&self, op: TokenKind) -> Option<(UnaryOp, u8)> {
match op { match op {
TokenKind::Minus => Some((UnaryOp::Neg, 30)), TokenKind::Minus => Some((UnaryOp::Neg, 30)),
TokenKind::Bang => Some((UnaryOp::Not, 30)),
_ => None, _ => None,
} }
@@ -490,6 +491,13 @@ impl<'src> Parser<'src> {
/// or `None` if the [TokenKind] is not a valid infix operator. /// or `None` if the [TokenKind] is not a valid infix operator.
fn infix_operator(&self, op: TokenKind) -> Option<(BinaryOp, u8, u8)> { fn infix_operator(&self, op: TokenKind) -> Option<(BinaryOp, u8, u8)> {
match op { 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::Plus => Some((BinaryOp::Add, 10, 11)),
TokenKind::Minus => Some((BinaryOp::Sub, 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)
})
);
}
} }
+66 -2
View File
@@ -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 } => { ExprKind::Binary { op, lhs, rhs } => {
let typed_lhs = self.analyze_expr(lhs); let typed_lhs = self.analyze_expr(lhs);
let typed_rhs = self.analyze_expr(rhs); let typed_rhs = self.analyze_expr(rhs);
@@ -356,8 +375,24 @@ impl Sema {
self.errors.push(SemanticError::new(e, expr.span)); self.errors.push(SemanticError::new(e, expr.span));
} }
let result_ty = typed_lhs.ty.clone(); let is_comparison = matches!(
self.deferred_binary.push((expr.span, result_ty.clone())); 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 { TypedExpr {
kind: TypedExprKind::Binary { kind: TypedExprKind::Binary {
op: *op, op: *op,
@@ -628,4 +663,33 @@ mod test {
.contains("unary minus only works on integer types") .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")
}));
}
} }
+18
View File
@@ -70,6 +70,16 @@ pub enum TokenKind {
Slash, Slash,
Percent, Percent,
Equal,
Unequal,
LessThan,
LessEqual,
GreaterThan,
GreaterEqual,
Assign,
Bang,
// Punctuation // Punctuation
Dot, Dot,
Comma, Comma,
@@ -109,6 +119,14 @@ impl Display for TokenKind {
TokenKind::Star => "`*`", TokenKind::Star => "`*`",
TokenKind::Slash => "`/`", TokenKind::Slash => "`/`",
TokenKind::Percent => "`%`", TokenKind::Percent => "`%`",
TokenKind::Equal => "`==`",
TokenKind::Unequal => "`!=`",
TokenKind::LessThan => "`<`",
TokenKind::LessEqual => "`<=`",
TokenKind::GreaterThan => "`>`",
TokenKind::GreaterEqual => "`>=`",
TokenKind::Assign => "`=`",
TokenKind::Bang => "`!`",
TokenKind::Dot => "`.`", TokenKind::Dot => "`.`",
TokenKind::Comma => "`,`", TokenKind::Comma => "`,`",
TokenKind::Colon => "`:`", TokenKind::Colon => "`:`",
+27
View File
@@ -0,0 +1,27 @@
#include <stdbool.h>
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
}
+7
View File
@@ -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; }
+1 -1
View File
@@ -1,3 +1,3 @@
fn main() -> i32 { fn main() -> i32 {
return 42; return 10 * 4 + 2;
} }