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:
@@ -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!(
|
||||
|
||||
Reference in New Issue
Block a user