diff --git a/src/ast.rs b/src/ast.rs index 59672fb..42d6d25 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -146,6 +146,7 @@ pub struct IfThenStmt { #[derive(Debug, Clone)] pub enum Stmt { Assign(LhsExprNode, ExprNode), + AssignOp(LhsExprNode, BinOp, ExprNode), VarDecl(Variable, ExprNode), Expr(ExprNode), Block(Vec), diff --git a/src/ast_walk_interpreter.rs b/src/ast_walk_interpreter.rs index 38ef3bf..4a219f2 100644 --- a/src/ast_walk_interpreter.rs +++ b/src/ast_walk_interpreter.rs @@ -68,6 +68,9 @@ impl AstWalkInterpreter { match s.data { Stmt::VarDecl(ref variable, ref expr) => self.eval_stmt_var_decl(variable, expr), Stmt::Assign(ref lhs_expr, ref expr) => self.eval_stmt_assign(lhs_expr, expr), + Stmt::AssignOp(ref lhs_expr, ref op, ref expr) => { + self.eval_stmt_assign_with_op(lhs_expr, op, expr, s) + } Stmt::Block(ref statements) => self.eval_stmt_block(statements), Stmt::Expr(ref expr) => { let val = self.eval_expr(expr)?; @@ -149,6 +152,42 @@ impl AstWalkInterpreter { Ok(StmtResult::None) } + fn eval_stmt_assign_with_op(&mut self, + lhs_expr: &LhsExprNode, + op: &BinOp, + expr: &ExprNode, + stmt: &StmtNode) + -> Result { + let val = self.eval_expr_as_value(expr)?; + match lhs_expr.data { + LhsExpr::Identifier(ref id) => { + let prev_expr_val = match self.env.borrow_mut().get_value(id) { + Some(v) => v, + None => { + return Err((RuntimeError::ReferenceError(id.to_owned()), lhs_expr.pos)); + } + }; + let retval = match *op { + BinOp::Add => operations::add(prev_expr_val, val), + BinOp::Sub => operations::subtract(prev_expr_val, val), + BinOp::Mul => operations::multiply(prev_expr_val, val), + BinOp::Div => operations::divide(prev_expr_val, val), + BinOp::Mod => operations::modulo(prev_expr_val, val), + BinOp::Lt | BinOp::Lte | BinOp::Gt | BinOp::Gte | BinOp::Eq => unreachable!(), + }; + let new_val = match retval { + Ok(val) => val, + Err(e) => { + return Err((e, stmt.pos)); + } + }; + // id must exist, because it was checked above + self.env.borrow_mut().set(id, new_val); + } + }; + Ok(StmtResult::None) + } + fn eval_stmt_block(&mut self, statements: &[StmtNode]) -> Result { diff --git a/src/grammar.rustpeg b/src/grammar.rustpeg index 688d226..94bf8b8 100644 --- a/src/grammar.rustpeg +++ b/src/grammar.rustpeg @@ -26,6 +26,16 @@ assignment_statement -> Stmt = lpos:#position i:identifier rpos:#position __ EQUALS __ e:expr_node __ TERMINATOR { Stmt::Assign(LhsExprNode { pos: (lpos, rpos), data: LhsExpr::Identifier(i) }, e) } + / lpos:#position i:identifier rpos:#position __ op:assign_with_op __ e:expr_node __ TERMINATOR { + Stmt::AssignOp(LhsExprNode { pos: (lpos, rpos), data: LhsExpr::Identifier(i) }, op, e) + } + +assign_with_op -> BinOp + = OP_PLUS __ EQUALS { BinOp::Add } + / OP_MINUS __ EQUALS { BinOp::Sub } + / OP_ASTERISK __ EQUALS { BinOp::Mul } + / OP_SLASH __ EQUALS { BinOp::Div } + / OP_MOD __ EQUALS { BinOp::Mod } variable_declaration -> Stmt = b:binding_type __ i:identifier __ EQUALS __ e:expr_node __ TERMINATOR { diff --git a/src/typechecker.rs b/src/typechecker.rs index 973a7f5..b496f23 100644 --- a/src/typechecker.rs +++ b/src/typechecker.rs @@ -289,6 +289,10 @@ impl TypeChecker { self.check_statement_assignment(lhs_expr, expr); StmtEffect::None } + Stmt::AssignOp(ref lhs_expr, ref op, ref expr) => { + self.check_statement_assignment_with_op(lhs_expr, op, expr, s); + StmtEffect::None + } Stmt::Block(ref statements) => { let current_env = self.env.clone(); self.env = TypeEnvironment::create_child(current_env.clone()); @@ -392,6 +396,47 @@ impl TypeChecker { }; } + fn check_statement_assignment_with_op(&mut self, + lhs_expr: &LhsExprNode, + op: &BinOp, + expr: &ExprNode, + stmt: &StmtNode) { + let checked_type = self.check_expr_as_value(expr); + match lhs_expr.data { + LhsExpr::Identifier(ref id) => { + let prev_type = match self.env.borrow_mut().get_type(id) { + Some(t) => t, + None => { + self.issues + .push((RuntimeError::ReferenceError(id.to_owned()).into(), + lhs_expr.pos)); + Type::Any + } + }; + let retval = match *op { + BinOp::Add => check_add_for_types(&prev_type, &checked_type), + ref op @ BinOp::Sub | + ref op @ BinOp::Mul | + ref op @ BinOp::Div | + ref op @ BinOp::Mod => { + check_binary_arithmetic_for_types(op.clone(), &prev_type, &checked_type) + } + _ => unreachable!(), + }; + let new_type = match retval { + Ok(t) => t, + Err(issue) => { + self.issues.push((issue, stmt.pos)); + Type::Any + } + }; + + // if id does not exist, then error was reported above + self.env.borrow_mut().set(id, new_type); + } + }; + } + fn check_statement_if_then_else(&mut self, statement: &StmtNode, if_then_stmt: &IfThenStmt) diff --git a/tests/run-pass/compound-assign.bl b/tests/run-pass/compound-assign.bl new file mode 100644 index 0000000..80a6dd3 --- /dev/null +++ b/tests/run-pass/compound-assign.bl @@ -0,0 +1,13 @@ +var s = "Hello"; +s += ", world!"; +assert_eq(s, "Hello, world!"); + +var x = 5; +x += 5; +assert_eq(x, 10); +x -= 5; +assert_eq(x, 5); +x *= 2; +assert_eq(x, 10); +x /= 2; +assert_eq(x, 5); diff --git a/tests/typecheck-fail/compound-assign-binary-error.bl b/tests/typecheck-fail/compound-assign-binary-error.bl new file mode 100644 index 0000000..ce94fef --- /dev/null +++ b/tests/typecheck-fail/compound-assign-binary-error.bl @@ -0,0 +1,2 @@ +var x = true; +x += 1; diff --git a/tests/typecheck-fail/compound-assign-binary-error.err b/tests/typecheck-fail/compound-assign-binary-error.err new file mode 100644 index 0000000..b6d6c8e --- /dev/null +++ b/tests/typecheck-fail/compound-assign-binary-error.err @@ -0,0 +1 @@ +[(RuntimeError(BinaryTypeError(Add, Bool, Number)), (14, 21))] diff --git a/tests/typecheck-pass/compound-assign.bl b/tests/typecheck-pass/compound-assign.bl new file mode 100644 index 0000000..15d9cb9 --- /dev/null +++ b/tests/typecheck-pass/compound-assign.bl @@ -0,0 +1,8 @@ +var s = "Hello"; +s += ", world!"; + +var x = 5; +x += 5; +x -= 5; +x *= 2; +x /= 2;