Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added src/.DS_Store
Binary file not shown.
2 changes: 2 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BinaryOp>,
pub expr: Expr,
}

Expand Down
29 changes: 29 additions & 0 deletions src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"))?;
Expand Down Expand Up @@ -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(),
Expand Down
53 changes: 49 additions & 4 deletions src/lexer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ pub enum Token {
Star,
Slash,
Percent,
PlusEqual,
MinusEqual,
StarEqual,
SlashEqual,
PercentEqual,
LeftParen,
RightParen,
LeftBrace,
Expand Down Expand Up @@ -46,15 +51,30 @@ pub fn tokenize(source: &str) -> Result<Vec<Token>, 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();
Expand Down Expand Up @@ -82,12 +102,21 @@ pub fn tokenize(source: &str) -> Result<Vec<Token>, 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();
Expand Down Expand Up @@ -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");
Expand Down
74 changes: 65 additions & 9 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?;
Expand All @@ -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)",
Expand All @@ -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<VarAssignStatement, ParseError> {
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<Expr, ParseError> {
self.parse_logical_or()
}
Expand Down Expand Up @@ -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()
Expand All @@ -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![
Expand Down
24 changes: 24 additions & 0 deletions tests/compiler_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading