Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
in a consistent alphabetical order.
([Zbyněk Juřica](https://github.com/zbyju))

- The compiler now issues a more specific error message when invalid assignment
shorthands are used, such as `++` or `*=`.
([0xda157](https://github.com/0xda157))

### Build tool

- The build tool now generates Hexdocs URLs using the new format of
Expand Down
9 changes: 9 additions & 0 deletions compiler-core/src/parse/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ pub enum LexicalErrorType {
tok: char,
},
InvalidTripleEqual,
/// `++`, `-=`, etc.
InvalidAssignmentShorthand,
/// A character was encountered that visually looks like a correct
/// character, but in reality it's some other unicode characters.
/// For example, a non-breaking-space instead of a regular space.
Expand Down Expand Up @@ -952,6 +954,13 @@ impl LexicalError {
"See: https://tour.gleam.run/basics/equality".into(),
],
),
LexicalErrorType::InvalidAssignmentShorthand => (
"Invalid assignment shorthand",
vec![
"Gleam does not support this assignment shorthand syntax, nor mutation".into(),
"Hint: variables may be rebound with `let` and `=`".into(),
],
),
LexicalErrorType::VisuallySimilarInvalidCharacter { name, correct } => (
"Unexpected character",
vec![wrap(&format!(
Expand Down
136 changes: 105 additions & 31 deletions compiler-core/src/parse/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,27 @@ where
'+' => {
let tok_start = self.get_pos();
let _ = self.next_char();
if let Some('.') = self.chr0 {
let _ = self.next_char();
let tok_end = self.get_pos();
self.emit((tok_start, Token::PlusDot, tok_end));
} else {
let tok_end = self.get_pos();
self.emit((tok_start, Token::Plus, tok_end));
match self.chr0 {
Some('.') => {
let _ = self.next_char();
let tok_end = self.get_pos();
self.emit((tok_start, Token::PlusDot, tok_end));
}
Some('=' | '+') => {
let _ = self.next_char();
let tok_end = self.get_pos();
return Err(LexicalError {
error: LexicalErrorType::InvalidAssignmentShorthand,
location: SrcSpan {
start: tok_start,
end: tok_end,
},
});
}
_ => {
let tok_end = self.get_pos();
self.emit((tok_start, Token::Plus, tok_end));
}
}
}
'*' => {
Expand All @@ -238,6 +252,17 @@ where
let tok_end = self.get_pos();
self.emit((tok_start, Token::StarDot, tok_end));
}
Some('=') => {
let _ = self.next_char();
let tok_end = self.get_pos();
return Err(LexicalError {
error: LexicalErrorType::InvalidAssignmentShorthand,
location: SrcSpan {
start: tok_start,
end: tok_end,
},
});
}
_ => {
let tok_end = self.get_pos();
self.emit((tok_start, Token::Star, tok_end));
Expand All @@ -258,46 +283,82 @@ where
let comment = self.lex_comment();
self.emit(comment);
}
Some('=') => {
let _ = self.next_char();
let tok_end = self.get_pos();
return Err(LexicalError {
error: LexicalErrorType::InvalidAssignmentShorthand,
location: SrcSpan {
start: tok_start,
end: tok_end,
},
});
}
_ => {
let tok_end = self.get_pos();
self.emit((tok_start, Token::Slash, tok_end));
}
}
}
'%' => {
self.eat_single_char(Token::Percent);
let tok_start = self.get_pos();
let _ = self.next_char();
match self.chr0 {
Some('=') => {
let _ = self.next_char();
let tok_end = self.get_pos();
return Err(LexicalError {
error: LexicalErrorType::InvalidAssignmentShorthand,
location: SrcSpan {
start: tok_start,
end: tok_end,
},
});
}
_ => {
let tok_end = self.get_pos();
self.emit((tok_start, Token::Percent, tok_end));
}
}
}
'|' => {
let tok_start = self.get_pos();
let _ = self.next_char();
if let Some('|') = self.chr0 {
let _ = self.next_char();
let tok_end = self.get_pos();
self.emit((tok_start, Token::VbarVbar, tok_end));
} else if let Some('>') = self.chr0 {
let _ = self.next_char();
let tok_end = self.get_pos();
self.emit((tok_start, Token::Pipe, tok_end));
} else {
let tok_end = self.get_pos();
self.emit((tok_start, Token::Vbar, tok_end));
match self.chr0 {
Some('|') => {
let _ = self.next_char();
let tok_end = self.get_pos();
self.emit((tok_start, Token::VbarVbar, tok_end));
}
Some('>') => {
let _ = self.next_char();
let tok_end = self.get_pos();
self.emit((tok_start, Token::Pipe, tok_end));
}
_ => {
let tok_end = self.get_pos();
self.emit((tok_start, Token::Vbar, tok_end));
}
}
}
'&' => {
let tok_start = self.get_pos();
let _ = self.next_char();
if let Some('&') = self.chr0 {
let _ = self.next_char();
let tok_end = self.get_pos();
self.emit((tok_start, Token::AmperAmper, tok_end));
} else {
return Err(LexicalError {
error: LexicalErrorType::UnrecognizedToken { tok: '&' },
location: SrcSpan {
start: tok_start,
end: tok_start,
},
});
match self.chr0 {
Some('&') => {
let _ = self.next_char();
let tok_end = self.get_pos();
self.emit((tok_start, Token::AmperAmper, tok_end));
}
_ => {
return Err(LexicalError {
error: LexicalErrorType::UnrecognizedToken { tok: '&' },
location: SrcSpan {
start: tok_start,
end: tok_start,
},
});
}
}
}
'-' => {
Expand All @@ -314,6 +375,19 @@ where
let tok_end = self.get_pos();
self.emit((tok_start, Token::RArrow, tok_end));
}
// Not including `-` here because repeated int negation is
// valid gleam
Some('=') => {
let _ = self.next_char();
let tok_end = self.get_pos();
return Err(LexicalError {
error: LexicalErrorType::InvalidAssignmentShorthand,
location: SrcSpan {
start: tok_start,
end: tok_end,
},
});
}
_ => {
let tok_end = self.get_pos();
self.emit((tok_start, Token::Minus, tok_end));
Expand Down
102 changes: 102 additions & 0 deletions compiler-core/src/parse/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,108 @@ fn triple_equals_with_whitespace() {
);
}

#[test]
fn invalid_assignment_shorthand_plus_equals() {
assert_error!(
"let wobble: Int = 32
wobble += 1",
ParseError {
error: ParseErrorType::LexError {
error: LexicalError {
error: LexicalErrorType::InvalidAssignmentShorthand,
location: SrcSpan { start: 36, end: 38 },
},
},
location: SrcSpan { start: 36, end: 38 },
}
);
}

#[test]
fn invalid_assignment_shorthand_plus_plus() {
assert_error!(
"let wobble: Int = 32
wobble++",
ParseError {
error: ParseErrorType::LexError {
error: LexicalError {
error: LexicalErrorType::InvalidAssignmentShorthand,
location: SrcSpan { start: 35, end: 37 },
},
},
location: SrcSpan { start: 35, end: 37 },
}
);
}

#[test]
fn invalid_assignment_shorthand_minus_equals() {
assert_error!(
"let wobble: Int = 32
wobble -= 1",
ParseError {
error: ParseErrorType::LexError {
error: LexicalError {
error: LexicalErrorType::InvalidAssignmentShorthand,
location: SrcSpan { start: 36, end: 38 },
},
},
location: SrcSpan { start: 36, end: 38 },
}
);
}

#[test]
fn invalid_assignment_shorthand_times_equals() {
assert_error!(
"let wobble: Int = 32
wobble *= 1",
ParseError {
error: ParseErrorType::LexError {
error: LexicalError {
error: LexicalErrorType::InvalidAssignmentShorthand,
location: SrcSpan { start: 36, end: 38 },
},
},
location: SrcSpan { start: 36, end: 38 },
}
);
}

#[test]
fn invalid_assignment_shorthand_divide_equals() {
assert_error!(
"let wobble: Int = 32
wobble /= 1",
ParseError {
error: ParseErrorType::LexError {
error: LexicalError {
error: LexicalErrorType::InvalidAssignmentShorthand,
location: SrcSpan { start: 36, end: 38 },
},
},
location: SrcSpan { start: 36, end: 38 },
}
);
}

#[test]
fn invalid_assignment_shorthand_modulo_equals() {
assert_error!(
"let wobble: Int = 32
wobble %= 1",
ParseError {
error: ParseErrorType::LexError {
error: LexicalError {
error: LexicalErrorType::InvalidAssignmentShorthand,
location: SrcSpan { start: 36, end: 38 },
},
},
location: SrcSpan { start: 36, end: 38 },
}
);
}

// https://github.com/gleam-lang/gleam/issues/1231
#[test]
fn pointless_spread() {
Expand Down
Loading