From 7c249deeab584fe8ef36f57c7419c3f2a8e26d8e Mon Sep 17 00:00:00 2001 From: Adam Hameed Date: Wed, 15 Jul 2026 00:30:40 -0400 Subject: [PATCH] feat: support bitwise operators (&, |, ^, <<, >>, ~) - New Pipe/Caret/Tilde/LessLess/GreaterGreater tokens; a single | is now valid instead of a lex error - BinaryOp gains BitAnd/BitOr/BitXor/ShiftLeft/ShiftRight, UnaryOp gains BitNot, with C's precedence: shift sits between additive and relational, and equality > & > ^ > | > && - Binary & is disambiguated from address-of naturally by parse position; x & &y parses as BitAnd(x, AddrOf(y)) - >> lowers to an arithmetic shift (ashr) to match C's sign-preserving behavior on negative ints; ~ lowers to LLVM not - Lexer/parser/precedence unit tests and e2e coverage including a popcount program; verified against clang Co-Authored-By: Claude Fable 5 --- src/ast/mod.rs | 6 ++ src/codegen/mod.rs | 25 ++++++- src/lexer/mod.rs | 65 ++++++++++++++--- src/parser/mod.rs | 161 +++++++++++++++++++++++++++++++++++++++++- tests/compiler_e2e.rs | 36 ++++++++++ 5 files changed, 277 insertions(+), 16 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index af92bb2..ccb28ff 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -122,6 +122,7 @@ pub enum UnaryOp { Negate, Posate, LogicalNot, + BitNot, Deref, AddrOf, } @@ -148,6 +149,11 @@ pub enum BinaryOp { GreaterEqual, LogicalAnd, LogicalOr, + BitAnd, + BitOr, + BitXor, + ShiftLeft, + ShiftRight, } // TODO: Add source spans to AST nodes for richer diagnostics. diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 996b0a4..069ea2a 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -195,7 +195,9 @@ fn type_of_expr( found.ok_or_else(|| format!("undefined variable '{}'", var.name)) } Expr::Unary(unary) => match unary.operator { - UnaryOp::Negate | UnaryOp::Posate | UnaryOp::LogicalNot => Ok(Type::Int), + UnaryOp::Negate | UnaryOp::Posate | UnaryOp::LogicalNot | UnaryOp::BitNot => { + Ok(Type::Int) + } UnaryOp::Deref => { let inner_ty = type_of_expr(&unary.expr, variables, function_types)?; match inner_ty { @@ -313,7 +315,7 @@ fn emit_expr<'ctx>( )?; Ok(BasicValueEnum::PointerValue(ptr)) } - UnaryOp::Negate | UnaryOp::Posate | UnaryOp::LogicalNot => { + UnaryOp::Negate | UnaryOp::Posate | UnaryOp::LogicalNot | UnaryOp::BitNot => { let operand = emit_expr( context, builder, @@ -342,6 +344,9 @@ fn emit_expr<'ctx>( .build_int_z_extend(cmp, context.i32_type(), "casttmp") .map_err(|err| format!("failed to emit zext for logical not: {err}"))? } + UnaryOp::BitNot => builder + .build_not(operand_int, "bnottmp") + .map_err(|err| format!("failed to emit not instruction: {err}"))?, _ => unreachable!(), }; Ok(BasicValueEnum::IntValue(res)) @@ -549,6 +554,22 @@ fn emit_expr<'ctx>( BinaryOp::Modulo => builder .build_int_signed_rem(left, right, "remtmp") .map_err(|err| format!("failed to emit rem instruction: {err}"))?, + BinaryOp::BitAnd => builder + .build_and(left, right, "andtmp") + .map_err(|err| format!("failed to emit and instruction: {err}"))?, + BinaryOp::BitOr => builder + .build_or(left, right, "ortmp") + .map_err(|err| format!("failed to emit or instruction: {err}"))?, + BinaryOp::BitXor => builder + .build_xor(left, right, "xortmp") + .map_err(|err| format!("failed to emit xor instruction: {err}"))?, + BinaryOp::ShiftLeft => builder + .build_left_shift(left, right, "shltmp") + .map_err(|err| format!("failed to emit shl instruction: {err}"))?, + BinaryOp::ShiftRight => builder + // Arithmetic shift: C's >> on signed ints preserves the sign bit + .build_right_shift(left, right, true, "ashrtmp") + .map_err(|err| format!("failed to emit ashr instruction: {err}"))?, BinaryOp::Equal | BinaryOp::NotEqual | BinaryOp::LessThan diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index 3f5b899..7961615 100644 --- a/src/lexer/mod.rs +++ b/src/lexer/mod.rs @@ -38,6 +38,11 @@ pub enum Token { OrOr, Exclamation, Ampersand, + Pipe, + Caret, + Tilde, + LessLess, + GreaterGreater, Comma, Question, Colon, @@ -186,20 +191,30 @@ pub fn tokenize(source: &str) -> Result, String> { } '<' => { chars.next(); - if chars.peek() == Some(&'=') { - chars.next(); - tokens.push(Token::LessEqual); - } else { - tokens.push(Token::LessThan); + match chars.peek() { + Some(&'=') => { + chars.next(); + tokens.push(Token::LessEqual); + } + Some(&'<') => { + chars.next(); + tokens.push(Token::LessLess); + } + _ => tokens.push(Token::LessThan), } } '>' => { chars.next(); - if chars.peek() == Some(&'=') { - chars.next(); - tokens.push(Token::GreaterEqual); - } else { - tokens.push(Token::GreaterThan); + match chars.peek() { + Some(&'=') => { + chars.next(); + tokens.push(Token::GreaterEqual); + } + Some(&'>') => { + chars.next(); + tokens.push(Token::GreaterGreater); + } + _ => tokens.push(Token::GreaterThan), } } '&' => { @@ -217,9 +232,17 @@ pub fn tokenize(source: &str) -> Result, String> { chars.next(); tokens.push(Token::OrOr); } else { - return Err("expected '|' after '|'".to_string()); + tokens.push(Token::Pipe); } } + '^' => { + chars.next(); + tokens.push(Token::Caret); + } + '~' => { + chars.next(); + tokens.push(Token::Tilde); + } '0'..='9' => { let mut number = String::new(); @@ -502,4 +525,24 @@ mod tests { ] ); } + + #[test] + fn tokenizes_bitwise_operators() { + let source = "| ^ ~ << >> < < > >"; + let tokens = tokenize(source).expect("should succeed"); + assert_eq!( + tokens, + vec![ + Token::Pipe, + Token::Caret, + Token::Tilde, + Token::LessLess, + Token::GreaterGreater, + Token::LessThan, + Token::LessThan, + Token::GreaterThan, + Token::GreaterThan, + ] + ); + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2b2fe89..a06e3ae 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -334,12 +334,12 @@ impl<'a> Parser<'a> { } fn parse_logical_and(&mut self) -> Result { - let mut expr = self.parse_equality()?; + let mut expr = self.parse_bit_or()?; loop { if self.peek() == Some(&Token::AndAnd) { self.next(); - let right = self.parse_equality()?; + let right = self.parse_bit_or()?; expr = Expr::Binary(BinaryExpr { left: Box::new(expr), operator: BinaryOp::LogicalAnd, @@ -353,6 +353,54 @@ impl<'a> Parser<'a> { Ok(expr) } + fn parse_bit_or(&mut self) -> Result { + let mut expr = self.parse_bit_xor()?; + + while self.peek() == Some(&Token::Pipe) { + self.next(); + let right = self.parse_bit_xor()?; + expr = Expr::Binary(BinaryExpr { + left: Box::new(expr), + operator: BinaryOp::BitOr, + right: Box::new(right), + }); + } + + Ok(expr) + } + + fn parse_bit_xor(&mut self) -> Result { + let mut expr = self.parse_bit_and()?; + + while self.peek() == Some(&Token::Caret) { + self.next(); + let right = self.parse_bit_and()?; + expr = Expr::Binary(BinaryExpr { + left: Box::new(expr), + operator: BinaryOp::BitXor, + right: Box::new(right), + }); + } + + Ok(expr) + } + + fn parse_bit_and(&mut self) -> Result { + let mut expr = self.parse_equality()?; + + while self.peek() == Some(&Token::Ampersand) { + self.next(); + let right = self.parse_equality()?; + expr = Expr::Binary(BinaryExpr { + left: Box::new(expr), + operator: BinaryOp::BitAnd, + right: Box::new(right), + }); + } + + Ok(expr) + } + fn parse_equality(&mut self) -> Result { let mut expr = self.parse_relational()?; @@ -376,7 +424,7 @@ impl<'a> Parser<'a> { } fn parse_relational(&mut self) -> Result { - let mut expr = self.parse_additive()?; + let mut expr = self.parse_shift()?; loop { let operator = match self.peek() { @@ -388,6 +436,28 @@ impl<'a> Parser<'a> { }; self.next(); + let right = self.parse_shift()?; + expr = Expr::Binary(BinaryExpr { + left: Box::new(expr), + operator, + right: Box::new(right), + }); + } + + Ok(expr) + } + + fn parse_shift(&mut self) -> Result { + let mut expr = self.parse_additive()?; + + loop { + let operator = match self.peek() { + Some(Token::LessLess) => BinaryOp::ShiftLeft, + Some(Token::GreaterGreater) => BinaryOp::ShiftRight, + _ => break, + }; + self.next(); + let right = self.parse_additive()?; expr = Expr::Binary(BinaryExpr { left: Box::new(expr), @@ -470,6 +540,14 @@ impl<'a> Parser<'a> { expr: Box::new(expr), })) } + Some(Token::Tilde) => { + self.next(); + let expr = self.parse_unary()?; + Ok(Expr::Unary(UnaryExpr { + operator: UnaryOp::BitNot, + expr: Box::new(expr), + })) + } Some(Token::Star) => { self.next(); let expr = self.parse_unary()?; @@ -982,6 +1060,83 @@ mod tests { assert_eq!(error, ParseError::InvalidIncrementTarget); } + #[test] + fn bitwise_operators_follow_c_precedence() { + // `1 & 2 == 2` must parse as `1 & (2 == 2)` — equality binds tighter + // than `&` in C. + let tokens = vec![ + Token::Int, + Token::Identifier("main".to_string()), + Token::LeftParen, + Token::RightParen, + Token::LeftBrace, + Token::Return, + Token::Integer(1), + Token::Ampersand, + Token::Integer(2), + Token::EqualEqual, + Token::Integer(2), + Token::Semicolon, + Token::RightBrace, + ]; + + let program = parse(&tokens).expect("parser should accept bitwise expression"); + + assert_eq!( + program.functions[0].body[0], + Statement::Return(ReturnStatement { + expr: Expr::Binary(BinaryExpr { + left: Box::new(Expr::IntegerLiteral(IntegerLiteral { value: 1 })), + operator: BinaryOp::BitAnd, + right: Box::new(Expr::Binary(BinaryExpr { + left: Box::new(Expr::IntegerLiteral(IntegerLiteral { value: 2 })), + operator: BinaryOp::Equal, + right: Box::new(Expr::IntegerLiteral(IntegerLiteral { value: 2 })), + })), + }), + }) + ); + } + + #[test] + fn parses_bitand_of_address_of() { + // `x & &y` must lex as two Ampersands and parse as BitAnd(x, AddrOf(y)). + let tokens = vec![ + Token::Int, + Token::Identifier("main".to_string()), + Token::LeftParen, + Token::RightParen, + Token::LeftBrace, + Token::Return, + Token::Identifier("x".to_string()), + Token::Ampersand, + Token::Ampersand, + Token::Identifier("y".to_string()), + Token::Semicolon, + Token::RightBrace, + ]; + + let program = parse(&tokens).expect("parser should accept `x & &y`"); + + assert_eq!( + program.functions[0].body[0], + Statement::Return(ReturnStatement { + expr: Expr::Binary(BinaryExpr { + left: Box::new(Expr::Variable(VariableExpr { + name: "x".to_string(), + })), + operator: BinaryOp::BitAnd, + right: Box::new(Expr::Unary(UnaryExpr { + operator: UnaryOp::AddrOf, + expr: Box::new(Expr::Variable(VariableExpr { + name: "y".to_string(), + })), + })), + }), + }) + ); + } + #[test] fn parses_ternary_conditional() { let tokens = vec![ diff --git a/tests/compiler_e2e.rs b/tests/compiler_e2e.rs index e6d0b2f..80a87fb 100644 --- a/tests/compiler_e2e.rs +++ b/tests/compiler_e2e.rs @@ -33,6 +33,42 @@ fn evaluates_division_and_subtraction() { assert_program_exit_code("int main() { return 20 / 5 - 1; }\n", 3); } +#[test] +fn evaluates_bitwise_operators() { + assert_program_exit_code("int main() { return 12 & 10; }\n", 8); + assert_program_exit_code("int main() { return 12 | 10; }\n", 14); + assert_program_exit_code("int main() { return 12 ^ 10; }\n", 6); + assert_program_exit_code("int main() { return 1 << 5; }\n", 32); + assert_program_exit_code("int main() { return 64 >> 3; }\n", 8); + // ~5 = -6, which is 250 as an exit code + assert_program_exit_code("int main() { return ~5; }\n", 250); +} + +#[test] +fn right_shift_is_arithmetic() { + // -8 >> 1 must sign-extend to -4 (252 as an exit code), not 2147483644. + assert_program_exit_code("int main() { int x = -8; return (x >> 1) + 256; }\n", 252); +} + +#[test] +fn bitwise_operators_follow_c_precedence() { + // Shift binds tighter than relational: (1 << 3) > 7 is true. + assert_program_exit_code("int main() { return 1 << 3 > 7; }\n", 1); + // Equality binds tighter than &: 4 & (3 == 3) = 4 & 1 = 0. + assert_program_exit_code("int main() { return 4 & 3 == 3; }\n", 0); + // & binds tighter than ^ binds tighter than |: 1 | (2 ^ (6 & 3)) = 1 | 0 = 1... check with clang: 6&3=2, 2^2=0, 1|0=1. + assert_program_exit_code("int main() { return 1 | 2 ^ 6 & 3; }\n", 1); +} + +#[test] +fn evaluates_bit_manipulation_program() { + // Count set bits of 203 (11001011 -> 5 bits). + assert_program_exit_code( + "int popcount(int v) { int count = 0; while (v != 0) { count += v & 1; v = v >> 1; } return count; }\nint main() { return popcount(203); }\n", + 5, + ); +} + #[test] fn evaluates_ternary_conditional() { assert_program_exit_code("int main() { return 1 ? 10 : 20; }\n", 10);