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:
@@ -11,6 +11,20 @@ pub enum UnaryOp {
|
||||
AddrOf, // `&`
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CompoundAssignOp {
|
||||
Add, // `+=`
|
||||
Sub, // `-=`
|
||||
Mul, // `*=`
|
||||
Div, // `/=`
|
||||
Rem, // `%=`
|
||||
BitAnd, // `&=`
|
||||
BitOr, // `|=`
|
||||
BitXor, // `^=`
|
||||
Shl, // `<<=`
|
||||
Shr, // `>>=`
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BinaryOp {
|
||||
// Logical
|
||||
@@ -33,6 +47,9 @@ pub enum BinaryOp {
|
||||
Mul, // `*`
|
||||
Div, // `/`
|
||||
Rem, // `%`
|
||||
// Shift
|
||||
Shl, // `<<`
|
||||
Shr, // `>>`
|
||||
// Assignment (lowest precedence, right-associative)
|
||||
Assign, // `=`
|
||||
}
|
||||
@@ -123,6 +140,13 @@ pub enum ExprKind {
|
||||
lhs: Box<Expr>,
|
||||
rhs: Box<Expr>,
|
||||
},
|
||||
// Compound assignment: `lhs op= rhs` (expands to `lhs = lhs op rhs`)
|
||||
CompoundAssign {
|
||||
op: CompoundAssignOp,
|
||||
op_span: Span,
|
||||
lhs: Box<Expr>,
|
||||
rhs: Box<Expr>,
|
||||
},
|
||||
|
||||
// Postfix
|
||||
Field {
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -85,6 +85,10 @@ define_tokens! {
|
||||
Bang => "`!`",
|
||||
Tilde => "`~`",
|
||||
|
||||
// ── Shift operators ───────────────────────────────────────────────────────
|
||||
Shl => "`<<`",
|
||||
Shr => "`>>`",
|
||||
|
||||
// ── Comparison operators ──────────────────────────────────────────────────
|
||||
EqEq => "`==`",
|
||||
BangEq => "`!=`",
|
||||
@@ -95,6 +99,16 @@ define_tokens! {
|
||||
|
||||
// ── Assignment ────────────────────────────────────────────────────────────
|
||||
Eq => "`=`",
|
||||
PlusEq => "`+=`",
|
||||
MinusEq => "`-=`",
|
||||
StarEq => "`*=`",
|
||||
SlashEq => "`/=`",
|
||||
PercentEq => "`%=`",
|
||||
AmpEq => "`&=`",
|
||||
PipeEq => "`|=`",
|
||||
CaretEq => "`^=`",
|
||||
ShlEq => "`<<=`",
|
||||
ShrEq => "`>>=`",
|
||||
|
||||
// ── Punctuation ───────────────────────────────────────────────────────────
|
||||
Arrow => "`->`",
|
||||
|
||||
Reference in New Issue
Block a user