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

@@ -267,14 +267,63 @@ impl<'src> Lexer<'src> {
self.advance();
let kind = match c {
// ── Unambiguous single-character tokens ──────────────────────────
'+' => TokenKind::Plus,
'*' => TokenKind::Star,
'/' => TokenKind::Slash,
'%' => TokenKind::Percent,
'&' => TokenKind::Amp,
'|' => TokenKind::Pipe,
'^' => TokenKind::Caret,
// ── Tokens that may be the prefix of a compound-assignment ───────
'+' => {
if self.peek() == Some('=') {
self.advance();
TokenKind::PlusEq
} else {
TokenKind::Plus
}
}
'*' => {
if self.peek() == Some('=') {
self.advance();
TokenKind::StarEq
} else {
TokenKind::Star
}
}
'/' => {
if self.peek() == Some('=') {
self.advance();
TokenKind::SlashEq
} else {
TokenKind::Slash
}
}
'%' => {
if self.peek() == Some('=') {
self.advance();
TokenKind::PercentEq
} else {
TokenKind::Percent
}
}
'&' => {
if self.peek() == Some('=') {
self.advance();
TokenKind::AmpEq
} else {
TokenKind::Amp
}
}
'|' => {
if self.peek() == Some('=') {
self.advance();
TokenKind::PipeEq
} else {
TokenKind::Pipe
}
}
'^' => {
if self.peek() == Some('=') {
self.advance();
TokenKind::CaretEq
} else {
TokenKind::Caret
}
}
'~' => TokenKind::Tilde,
'.' => TokenKind::Dot,
'(' => TokenKind::LParen,
@@ -292,6 +341,9 @@ impl<'src> Lexer<'src> {
if self.peek() == Some('>') {
self.advance();
TokenKind::Arrow
} else if self.peek() == Some('=') {
self.advance();
TokenKind::MinusEq
} else {
TokenKind::Minus
}
@@ -313,7 +365,13 @@ impl<'src> Lexer<'src> {
}
}
'<' => {
if self.peek() == Some('=') {
if self.at_ascii2(b'<', b'=') {
self.pos += 2;
TokenKind::ShlEq
} else if self.peek() == Some('<') {
self.advance();
TokenKind::Shl
} else if self.peek() == Some('=') {
self.advance();
TokenKind::LtEq
} else {
@@ -321,7 +379,13 @@ impl<'src> Lexer<'src> {
}
}
'>' => {
if self.peek() == Some('=') {
if self.at_ascii2(b'>', b'=') {
self.pos += 2;
TokenKind::ShrEq
} else if self.peek() == Some('>') {
self.advance();
TokenKind::Shr
} else if self.peek() == Some('=') {
self.advance();
TokenKind::GtEq
} else {
@@ -518,6 +582,31 @@ mod tests {
);
}
#[test]
fn compound_assign_operators() {
let src = "+= -= *= /= %= &= |= ^= <<= >>=";
assert_eq!(
kinds(src),
vec![
PlusEq, MinusEq, StarEq, SlashEq, PercentEq, AmpEq, PipeEq, CaretEq, ShlEq,
ShrEq, Eof
]
);
}
#[test]
fn shift_operators() {
let src = "<< >> <<= >>=";
assert_eq!(kinds(src), vec![Shl, Shr, ShlEq, ShrEq, Eof]);
}
#[test]
fn shift_does_not_steal_comparison() {
// `< =` (with space) should be Lt then Eq, not LtEq
let src = "a < b > c";
assert_eq!(kinds(src), vec![Ident, Lt, Ident, Gt, Ident, Eof]);
}
#[test]
fn punctuation() {
assert_eq!(