Feat: add compound assignment and shift operators

Compound assignment: +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
Shift: <<, >>

Each compound assignment token parses at the same precedence as `=`
(right-associative, lowest) and produces ExprKind::CompoundAssign.
Shifts parse between additive and multiplicative precedence.
GRAMMAR.ebnf and SYNTAX.md updated accordingly.
This commit is contained in:
2026-03-10 18:29:52 +01:00
parent 1a4e464d5e
commit a82b7e4633
6 changed files with 269 additions and 56 deletions

View File

@@ -2,8 +2,9 @@ use std::fmt;
use crate::{
ast::{
BinaryOp, Block, ElseBranch, Expr, ExprKind, FieldDef, FuncDef, Param, Program, Stmt,
StmtKind, StructDef, StructField, TopLevelDef, TopLevelDefKind, Type, UnaryOp,
BinaryOp, Block, CompoundAssignOp, ElseBranch, Expr, ExprKind, FieldDef, FuncDef, Param,
Program, Stmt, StmtKind, StructDef, StructField, TopLevelDef, TopLevelDefKind, Type,
UnaryOp,
},
lexer::Lexer,
token::{Span, Token, TokenKind},
@@ -35,9 +36,18 @@ impl fmt::Display for ParseError {
fn infix_bp(kind: TokenKind) -> Option<(u8, u8)> {
let bp = match kind {
// Assignment: lowest precedence, right-associative (left_bp == right_bp).
// `a = b = c` → `a = (b = c)`.
TokenKind::Eq => (2, 2),
// Assignment and compound assignment: lowest precedence, right-associative.
TokenKind::Eq
| TokenKind::PlusEq
| TokenKind::MinusEq
| TokenKind::StarEq
| TokenKind::SlashEq
| TokenKind::PercentEq
| TokenKind::AmpEq
| TokenKind::PipeEq
| TokenKind::CaretEq
| TokenKind::ShlEq
| TokenKind::ShrEq => (2, 2),
TokenKind::Or => (10, 11),
TokenKind::And => (20, 21),
TokenKind::Pipe => (30, 31),
@@ -50,6 +60,7 @@ fn infix_bp(kind: TokenKind) -> Option<(u8, u8)> {
| TokenKind::LtEq
| TokenKind::GtEq => (55, 56),
TokenKind::Plus | TokenKind::Minus => (60, 61),
TokenKind::Shl | TokenKind::Shr => (65, 66),
TokenKind::Star | TokenKind::Slash | TokenKind::Percent => (70, 71),
// Postfix: `.`, `[`, `(` — handled separately in parse_led, bp listed
// here only so callers can detect them as infix/postfix operators.
@@ -82,6 +93,22 @@ fn token_to_unary_op(kind: TokenKind) -> UnaryOp {
}
}
fn token_to_compound_assign_op(kind: TokenKind) -> Option<CompoundAssignOp> {
match kind {
TokenKind::PlusEq => Some(CompoundAssignOp::Add),
TokenKind::MinusEq => Some(CompoundAssignOp::Sub),
TokenKind::StarEq => Some(CompoundAssignOp::Mul),
TokenKind::SlashEq => Some(CompoundAssignOp::Div),
TokenKind::PercentEq => Some(CompoundAssignOp::Rem),
TokenKind::AmpEq => Some(CompoundAssignOp::BitAnd),
TokenKind::PipeEq => Some(CompoundAssignOp::BitOr),
TokenKind::CaretEq => Some(CompoundAssignOp::BitXor),
TokenKind::ShlEq => Some(CompoundAssignOp::Shl),
TokenKind::ShrEq => Some(CompoundAssignOp::Shr),
_ => None,
}
}
fn token_to_binary_op(kind: TokenKind) -> BinaryOp {
match kind {
TokenKind::Or => BinaryOp::Or,
@@ -100,6 +127,8 @@ fn token_to_binary_op(kind: TokenKind) -> BinaryOp {
TokenKind::Star => BinaryOp::Mul,
TokenKind::Slash => BinaryOp::Div,
TokenKind::Percent => BinaryOp::Rem,
TokenKind::Shl => BinaryOp::Shl,
TokenKind::Shr => BinaryOp::Shr,
TokenKind::Eq => BinaryOp::Assign,
_ => unreachable!("not a binary op: {:?}", kind),
}
@@ -588,6 +617,22 @@ impl<'src> Parser<'src> {
)
}
// Compound assignment: `lhs op= rhs`
kind if token_to_compound_assign_op(kind).is_some() => {
let op = token_to_compound_assign_op(kind).unwrap();
let rhs = self.pratt(r_bp, allow_struct_lit);
let span = lhs.span.cover(rhs.span);
Expr::new(
ExprKind::CompoundAssign {
op,
op_span: op_tok.span,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
},
span,
)
}
// Binary operator
kind => {
let op = token_to_binary_op(kind);