From b06c8d72cf8086c8c7914dd53e3b7cb375605691 Mon Sep 17 00:00:00 2001 From: Adam Hameed Date: Wed, 15 Jul 2026 00:17:16 -0400 Subject: [PATCH] feat: support compound assignment operators (+=, -=, *=, /=, %=) - Lexer emits PlusEqual/MinusEqual/StarEqual/SlashEqual/PercentEqual - VarAssignStatement carries an optional compound BinaryOp; parsing is shared between statement position and for-loop post expressions - Codegen emits the lvalue once, then load-apply-store for compound ops, restricted to int targets - Unit tests for lexing/parsing plus e2e tests including compound assignment through a pointer and in for-post; verified against clang Co-Authored-By: Claude Fable 5 --- src/.DS_Store | Bin 0 -> 6148 bytes src/ast/mod.rs | 2 ++ src/codegen/mod.rs | 29 +++++++++++++++++ src/lexer/mod.rs | 53 +++++++++++++++++++++++++++--- src/parser/mod.rs | 74 +++++++++++++++++++++++++++++++++++++----- tests/compiler_e2e.rs | 24 ++++++++++++++ 6 files changed, 169 insertions(+), 13 deletions(-) create mode 100644 src/.DS_Store diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a4c0673aefd50216cdc0e1695a8246ae1ae0ccf3 GIT binary patch literal 6148 zcmeHKy-EW?5S}&B9NGl5uw1aTG57*!IG-Rdp!p#di5H@RUmV^~-SK8Q~s zXzw>W%Vjs0Sc>Qj%zk(CvoraQyU7xfX?{1Bb>5R@t!|?rWyv;o0Jj=SHybDWmbNg^*pI#3_Z~q(bh}IgM~7t9t%PD>A>QS0KhuTPH^pJJkYWMn0hP(VSyM)1xl*%M+_tB zuv;IOdMpGbos6rDeSBr(PbkJ!huykxGO3`h%78KuGf&u6N$->aW#CUS zVCq>XYvU*R*;@H=JZmlV2+G27g`i8p;>WSO;8DB{bpoH|3t;N85QGJye*_#2x+nub G%D@-BfQUH& literal 0 HcmV?d00001 diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d0faeb1..fdf8077 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -70,6 +70,8 @@ pub struct VarDeclareStatement { #[derive(Debug, Clone, PartialEq, Eq)] pub struct VarAssignStatement { pub target: Expr, + /// Compound operator for `+=`, `-=`, `*=`, `/=`, `%=`; `None` for plain `=`. + pub op: Option, pub expr: Expr, } diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index d96c1f5..3da9140 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -642,6 +642,34 @@ fn emit_statement<'ctx>( function_types, module, )?; + let val = if let Some(op) = assign.op { + let target_ty = type_of_expr(&assign.target, variables, function_types)?; + if target_ty != Type::Int { + return Err("compound assignment is only supported for int".to_string()); + } + let rhs = match val { + BasicValueEnum::IntValue(i) => i, + _ => { + return Err("compound assignment operand must be an integer".to_string()); + } + }; + let current = builder + .build_load(context.i32_type(), ptr, "loadtmp") + .map_err(|err| format!("failed to load assignment target: {err}"))? + .into_int_value(); + let combined = match op { + BinaryOp::Add => builder.build_int_add(current, rhs, "addtmp"), + BinaryOp::Subtract => builder.build_int_sub(current, rhs, "subtmp"), + BinaryOp::Multiply => builder.build_int_mul(current, rhs, "multmp"), + BinaryOp::Divide => builder.build_int_signed_div(current, rhs, "divtmp"), + BinaryOp::Modulo => builder.build_int_signed_rem(current, rhs, "remtmp"), + _ => return Err(format!("unsupported compound assignment operator {op:?}")), + } + .map_err(|err| format!("failed to emit compound assignment: {err}"))?; + combined.into() + } else { + val + }; builder .build_store(ptr, val) .map_err(|err| format!("failed to build store: {err}"))?; @@ -1037,6 +1065,7 @@ mod tests { target: Expr::Variable(VariableExpr { name: "x".to_string(), }), + op: None, expr: Expr::Binary(BinaryExpr { left: Box::new(Expr::Variable(VariableExpr { name: "x".to_string(), diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index afcdd34..a086cf1 100644 --- a/src/lexer/mod.rs +++ b/src/lexer/mod.rs @@ -13,6 +13,11 @@ pub enum Token { Star, Slash, Percent, + PlusEqual, + MinusEqual, + StarEqual, + SlashEqual, + PercentEqual, LeftParen, RightParen, LeftBrace, @@ -46,15 +51,30 @@ pub fn tokenize(source: &str) -> Result, String> { match ch { '+' => { chars.next(); - tokens.push(Token::Plus); + if chars.peek() == Some(&'=') { + chars.next(); + tokens.push(Token::PlusEqual); + } else { + tokens.push(Token::Plus); + } } '-' => { chars.next(); - tokens.push(Token::Minus); + if chars.peek() == Some(&'=') { + chars.next(); + tokens.push(Token::MinusEqual); + } else { + tokens.push(Token::Minus); + } } '*' => { chars.next(); - tokens.push(Token::Star); + if chars.peek() == Some(&'=') { + chars.next(); + tokens.push(Token::StarEqual); + } else { + tokens.push(Token::Star); + } } '/' => { chars.next(); @@ -82,12 +102,21 @@ pub fn tokenize(source: &str) -> Result, String> { return Err("unterminated block comment".to_string()); } } + Some(&'=') => { + chars.next(); + tokens.push(Token::SlashEqual); + } _ => tokens.push(Token::Slash), } } '%' => { chars.next(); - tokens.push(Token::Percent); + if chars.peek() == Some(&'=') { + chars.next(); + tokens.push(Token::PercentEqual); + } else { + tokens.push(Token::Percent); + } } '(' => { chars.next(); @@ -369,6 +398,22 @@ mod tests { assert!(error.contains("unterminated block comment")); } + #[test] + fn tokenizes_compound_assignment_operators() { + let tokens = tokenize("+= -= *= /= %=").expect("tokenization should succeed"); + + assert_eq!( + tokens, + vec![ + Token::PlusEqual, + Token::MinusEqual, + Token::StarEqual, + Token::SlashEqual, + Token::PercentEqual, + ] + ); + } + #[test] fn tokenizes_percent_operator() { let tokens = tokenize("7 % 3").expect("tokenization should succeed"); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4ababa6..f398a89 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -190,12 +190,8 @@ impl<'a> Parser<'a> { None } else { let target = self.parse_unary()?; - self.expect_token(Token::Equals, "`=`")?; - let expr = self.parse_expression()?; - Some(Box::new(Statement::Assign(VarAssignStatement { - target, - expr, - }))) + let assign = self.parse_assignment(target)?; + Some(Box::new(Statement::Assign(assign))) }; self.expect_right_paren()?; let body = self.parse_statement()?; @@ -208,10 +204,9 @@ impl<'a> Parser<'a> { } Some(_) => { let target = self.parse_unary()?; - self.expect_token(Token::Equals, "`=`")?; - let expr = self.parse_expression()?; + let assign = self.parse_assignment(target)?; self.expect_semicolon()?; - Ok(Statement::Assign(VarAssignStatement { target, expr })) + Ok(Statement::Assign(assign)) } None => Err(ParseError::UnexpectedToken { expected: "statement (return, variable declaration, assignment, block, if, while, or for)", @@ -220,6 +215,29 @@ impl<'a> Parser<'a> { } } + /// Parses the `= expr` / `op= expr` tail of an assignment statement, + /// given the already-parsed assignment target. + fn parse_assignment(&mut self, target: Expr) -> Result { + let op = match self.peek() { + Some(Token::Equals) => None, + Some(Token::PlusEqual) => Some(BinaryOp::Add), + Some(Token::MinusEqual) => Some(BinaryOp::Subtract), + Some(Token::StarEqual) => Some(BinaryOp::Multiply), + Some(Token::SlashEqual) => Some(BinaryOp::Divide), + Some(Token::PercentEqual) => Some(BinaryOp::Modulo), + other => { + return Err(ParseError::UnexpectedToken { + expected: "`=`, `+=`, `-=`, `*=`, `/=`, or `%=`", + found: other.cloned(), + }); + } + }; + self.next(); + + let expr = self.parse_expression()?; + Ok(VarAssignStatement { target, op, expr }) + } + fn parse_expression(&mut self) -> Result { self.parse_logical_or() } @@ -776,6 +794,7 @@ mod tests { target: Expr::Variable(VariableExpr { name: "x".to_string(), }), + op: None, expr: Expr::Binary(BinaryExpr { left: Box::new(Expr::Variable(VariableExpr { name: "x".to_string() @@ -793,6 +812,43 @@ mod tests { ); } + #[test] + fn parses_compound_assignment() { + let tokens = vec![ + Token::Int, + Token::Identifier("main".to_string()), + Token::LeftParen, + Token::RightParen, + Token::LeftBrace, + Token::Int, + Token::Identifier("x".to_string()), + Token::Equals, + Token::Integer(5), + Token::Semicolon, + Token::Identifier("x".to_string()), + Token::PlusEqual, + Token::Integer(2), + Token::Semicolon, + Token::Return, + Token::Identifier("x".to_string()), + Token::Semicolon, + Token::RightBrace, + ]; + + let program = parse(&tokens).expect("parser should accept compound assignment"); + + assert_eq!( + program.functions[0].body[1], + Statement::Assign(VarAssignStatement { + target: Expr::Variable(VariableExpr { + name: "x".to_string(), + }), + op: Some(BinaryOp::Add), + expr: Expr::IntegerLiteral(IntegerLiteral { value: 2 }), + }) + ); + } + #[test] fn parses_comparisons_and_logical_operators() { let tokens = vec![ diff --git a/tests/compiler_e2e.rs b/tests/compiler_e2e.rs index a3954a4..95bf7a4 100644 --- a/tests/compiler_e2e.rs +++ b/tests/compiler_e2e.rs @@ -33,6 +33,30 @@ fn evaluates_division_and_subtraction() { assert_program_exit_code("int main() { return 20 / 5 - 1; }\n", 3); } +#[test] +fn evaluates_compound_assignments() { + assert_program_exit_code( + "int main() { int x = 10; x += 5; x -= 3; x *= 4; x /= 6; x %= 5; return x; }\n", + 3, + ); +} + +#[test] +fn evaluates_compound_assignment_through_pointer() { + assert_program_exit_code( + "int main() { int x = 40; int *p = &x; *p += 2; return x; }\n", + 42, + ); +} + +#[test] +fn evaluates_compound_assignment_in_for_post() { + assert_program_exit_code( + "int main() { int total = 0; for (int i = 1; i <= 4; i += 1) { total += i; } return total; }\n", + 10, + ); +} + #[test] fn evaluates_modulo() { assert_program_exit_code("int main() { return 17 % 5; }\n", 2);