diff --git a/e2e.sh b/e2e.sh index fe57179..efbdcde 100755 --- a/e2e.sh +++ b/e2e.sh @@ -104,4 +104,7 @@ run_harness_test "tests/if-else.src" "tests/if-else.c" 0 # Test variable declarations and scoping. run_test "tests/let_stmt.src" 30 +# Test assignments and multi-assignments +run_test "tests/assign.src" 42 + echo "All end-to-end tests passed!" \ No newline at end of file diff --git a/src/frontend/ast.rs b/src/frontend/ast.rs index a794d90..256b20a 100644 --- a/src/frontend/ast.rs +++ b/src/frontend/ast.rs @@ -107,6 +107,9 @@ pub enum StmtKind { ty: P::ReturnType, initializer: Option>, }, + Expression { + expr: Expr

, + }, } #[derive(Debug, PartialEq, Eq)] @@ -136,6 +139,10 @@ pub enum ExprKind { lhs: Box>, rhs: Box>, }, + Assign { + lval: Box>, + rval: Box>, + }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src/frontend/parser.rs b/src/frontend/parser.rs index 77a1ef5..fe4d18b 100644 --- a/src/frontend/parser.rs +++ b/src/frontend/parser.rs @@ -297,11 +297,7 @@ impl<'src> Parser<'src> { TokenKind::If => self.parse_if_stmt(), TokenKind::Return => self.parse_return_stmt(), TokenKind::Let => self.parse_let_stmt(), - - _ => Err(ParseError::new( - format!("expected a statement but found {} instead", peek_token.kind), - peek_token.span, - )), + _ => self.parse_expr_stmt(), } } @@ -378,6 +374,22 @@ impl<'src> Parser<'src> { }) } + /// Parses an expression statement. + /// + /// ```ebnf + /// expr_stmt = expr ";" ; + /// ``` + fn parse_expr_stmt(&mut self) -> ParseResult { + let expr = self.parse_expr()?; + let semi_token = self.expect(TokenKind::Semicolon)?; + let span = expr.span.join(semi_token.span); + + Ok(Stmt { + kind: StmtKind::Expression { expr }, + span, + }) + } + /// Parses a return statement. /// /// ```ebnf @@ -456,6 +468,29 @@ impl<'src> Parser<'src> { loop { let peek_token = self.peek_no_eof()?; + if peek_token.kind == TokenKind::Assign { + let left_bp = 2; + let right_bp = 1; // Right-associative mapping + + if left_bp < min_bp { + break; + } + + self.advance(); // consume '=' + let rhs = self.parse_expr_bp(right_bp)?; + let span = lhs.span.join(rhs.span); + + lhs = Expr { + kind: ExprKind::Assign { + lval: Box::new(lhs), + rval: Box::new(rhs), + }, + ty: (), + span, + }; + continue; + } + let Some((op, left_bp, right_bp)) = self.infix_operator(peek_token.kind) else { break; // Not an infix operator }; @@ -875,6 +910,49 @@ mod test { ); } + #[test] + fn assign_expr_stmt() { + assert_eq!( + parse("a = b = 5;", Parser::parse_stmt), + Success(Stmt { + kind: StmtKind::Expression { + expr: Expr { + kind: ExprKind::Assign { + lval: Box::new(Expr { + kind: ExprKind::Identifier { + name: "a".to_string() + }, + ty: (), + span: Span::new(0, 1) + }), + rval: Box::new(Expr { + kind: ExprKind::Assign { + lval: Box::new(Expr { + kind: ExprKind::Identifier { + name: "b".to_string() + }, + ty: (), + span: Span::new(4, 5) + }), + rval: Box::new(Expr { + kind: ExprKind::Integer { value: 5 }, + ty: (), + span: Span::new(8, 9) + }) + }, + ty: (), + span: Span::new(4, 9) + }) + }, + ty: (), + span: Span::new(0, 9) + } + }, + span: Span::new(0, 10) + }) + ); + } + #[test] fn function_decl() { assert_eq!( diff --git a/src/frontend/sema.rs b/src/frontend/sema.rs index 5a74c7d..5483814 100644 --- a/src/frontend/sema.rs +++ b/src/frontend/sema.rs @@ -390,6 +390,14 @@ impl Sema { span: stmt.span, } } + StmtKind::Expression { expr } => { + let typed_expr = self.analyze_expr(expr); + + TypedStmt { + kind: TypedStmtKind::Expression { expr: typed_expr }, + span: stmt.span, + } + } } } @@ -518,6 +526,31 @@ impl Sema { span: expr.span, } } + ExprKind::Assign { lval, rval } => { + let typed_rval = self.analyze_expr(rval); + let typed_lval = self.analyze_expr(lval); + + match &typed_lval.kind { + TypedExprKind::Identifier { .. } => {} + _ => self.errors.push(SemanticError::new( + "invalid left-hand side of assignment", + lval.span, + )), + } + + if let Err(e) = self.unify(&typed_lval.ty, &typed_rval.ty) { + self.errors.push(SemanticError::new(e, expr.span)); + } + + TypedExpr { + ty: typed_rval.ty.clone(), + kind: TypedExprKind::Assign { + lval: Box::new(typed_lval), + rval: Box::new(typed_rval), + }, + span: expr.span, + } + } } } @@ -586,6 +619,9 @@ impl Sema { ty: self.apply_subst(&ty), initializer: initializer.map(|e| self.apply_subst_expr(e)), }, + TypedStmtKind::Expression { expr } => TypedStmtKind::Expression { + expr: self.apply_subst_expr(expr), + }, }; TypedStmt { kind, span } @@ -610,6 +646,10 @@ impl Sema { lhs: Box::new(self.apply_subst_expr(*lhs)), rhs: Box::new(self.apply_subst_expr(*rhs)), }, + TypedExprKind::Assign { lval, rval } => TypedExprKind::Assign { + lval: Box::new(self.apply_subst_expr(*lval)), + rval: Box::new(self.apply_subst_expr(*rval)), + }, }; TypedExpr { kind, ty, span } @@ -863,4 +903,34 @@ mod test { let src = "fn test() { let a; }"; assert!(analyze(src).is_err()); } + + #[test] + fn valid_expression_stmt() { + let src = "fn test() { 5 + 5; }"; + assert!(analyze(src).is_ok()); + } + + #[test] + fn valid_assignment() { + let src = "fn test() { let a = 5; a = 10; }"; + assert!(analyze(src).is_ok()); + } + + #[test] + fn invalid_assignment_type() { + let src = "fn test() { let a: i32 = 5; a = true; }"; + let errors = analyze(src).unwrap_err(); + assert!(errors.iter().any(|e| e.message.contains("type mismatch"))); + } + + #[test] + fn invalid_lvalue() { + let src = "fn test() { 5 = 10; }"; + let errors = analyze(src).unwrap_err(); + assert!( + errors + .iter() + .any(|e| e.message.contains("invalid left-hand side of assignment")) + ); + } } diff --git a/src/middle/builder.rs b/src/middle/builder.rs index 4c01cff..c11bdcc 100644 --- a/src/middle/builder.rs +++ b/src/middle/builder.rs @@ -272,6 +272,9 @@ impl FuncBuilder { }); } } + TypedStmtKind::Expression { expr } => { + self.lower_expr(expr); + } } } @@ -309,6 +312,21 @@ impl FuncBuilder { Operand::Copy(temp) } + TypedExprKind::Assign { lval, rval } => { + let rval_op = self.lower_expr(rval); + + let local_id = match &lval.kind { + TypedExprKind::Identifier { name } => self.lookup(name), + _ => panic!("invalid lval in MIR lowering"), + }; + + self.emit_stmt(Statement { + kind: StatementKind::Assign(local_id, Rvalue::Use(rval_op.clone())), + span: expr.span, + }); + + rval_op + } } } } diff --git a/src/middle/fold.rs b/src/middle/fold.rs index dc1a7f9..5a0c0d0 100644 --- a/src/middle/fold.rs +++ b/src/middle/fold.rs @@ -29,6 +29,9 @@ fn optimize_function(func: &mut MirFunction) { // Replace the complex instruction with a simple constant use *rvalue = Rvalue::Use(Operand::Constant(constant.clone())); known_constants.insert(*local, constant); + } else { + // Reassigned to a non-computable value; remove older cached inferences + known_constants.remove(local); } } diff --git a/tests/assign.src b/tests/assign.src new file mode 100644 index 0000000..f6fb5c2 --- /dev/null +++ b/tests/assign.src @@ -0,0 +1,6 @@ +fn main() -> i32 { + let a = 0; + let b = 0; + a = b = 42; + return a; +} \ No newline at end of file