Skip to content

Commit bf24cee

Browse files
committed
provide specific error message for invalid assignment shorthand
1 parent 9130e2b commit bf24cee

4 files changed

Lines changed: 220 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
in a consistent alphabetical order.
5656
([Zbyněk Juřica](https://github.com/zbyju))
5757

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

6064
- The build tool now generates Hexdocs URLs using the new format of

compiler-core/src/parse/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ pub enum LexicalErrorType {
4646
tok: char,
4747
},
4848
InvalidTripleEqual,
49+
/// `++`, `-=`, etc.
50+
InvalidAssignmentShorthand,
4951
/// A character was encountered that visually looks like a correct
5052
/// character, but in reality it's some other unicode characters.
5153
/// For example, a non-breaking-space instead of a regular space.
@@ -952,6 +954,13 @@ impl LexicalError {
952954
"See: https://tour.gleam.run/basics/equality".into(),
953955
],
954956
),
957+
LexicalErrorType::InvalidAssignmentShorthand => (
958+
"Invalid assignment shorthand",
959+
vec![
960+
"Gleam does not support this assignment shorthand syntax, nor mutation".into(),
961+
"Hint: variables may be rebound with `let` and `=`".into(),
962+
],
963+
),
955964
LexicalErrorType::VisuallySimilarInvalidCharacter { name, correct } => (
956965
"Unexpected character",
957966
vec![wrap(&format!(

compiler-core/src/parse/lexer.rs

Lines changed: 105 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,27 @@ where
220220
'+' => {
221221
let tok_start = self.get_pos();
222222
let _ = self.next_char();
223-
if let Some('.') = self.chr0 {
224-
let _ = self.next_char();
225-
let tok_end = self.get_pos();
226-
self.emit((tok_start, Token::PlusDot, tok_end));
227-
} else {
228-
let tok_end = self.get_pos();
229-
self.emit((tok_start, Token::Plus, tok_end));
223+
match self.chr0 {
224+
Some('.') => {
225+
let _ = self.next_char();
226+
let tok_end = self.get_pos();
227+
self.emit((tok_start, Token::PlusDot, tok_end));
228+
}
229+
Some('=') | Some('+') => {
230+
let _ = self.next_char();
231+
let tok_end = self.get_pos();
232+
return Err(LexicalError {
233+
error: LexicalErrorType::InvalidAssignmentShorthand,
234+
location: SrcSpan {
235+
start: tok_start,
236+
end: tok_end,
237+
},
238+
});
239+
}
240+
_ => {
241+
let tok_end = self.get_pos();
242+
self.emit((tok_start, Token::Plus, tok_end));
243+
}
230244
}
231245
}
232246
'*' => {
@@ -238,6 +252,17 @@ where
238252
let tok_end = self.get_pos();
239253
self.emit((tok_start, Token::StarDot, tok_end));
240254
}
255+
Some('=') => {
256+
let _ = self.next_char();
257+
let tok_end = self.get_pos();
258+
return Err(LexicalError {
259+
error: LexicalErrorType::InvalidAssignmentShorthand,
260+
location: SrcSpan {
261+
start: tok_start,
262+
end: tok_end,
263+
},
264+
});
265+
}
241266
_ => {
242267
let tok_end = self.get_pos();
243268
self.emit((tok_start, Token::Star, tok_end));
@@ -258,46 +283,82 @@ where
258283
let comment = self.lex_comment();
259284
self.emit(comment);
260285
}
286+
Some('=') => {
287+
let _ = self.next_char();
288+
let tok_end = self.get_pos();
289+
return Err(LexicalError {
290+
error: LexicalErrorType::InvalidAssignmentShorthand,
291+
location: SrcSpan {
292+
start: tok_start,
293+
end: tok_end,
294+
},
295+
});
296+
}
261297
_ => {
262298
let tok_end = self.get_pos();
263299
self.emit((tok_start, Token::Slash, tok_end));
264300
}
265301
}
266302
}
267303
'%' => {
268-
self.eat_single_char(Token::Percent);
304+
let tok_start = self.get_pos();
305+
let _ = self.next_char();
306+
match self.chr0 {
307+
Some('=') => {
308+
let _ = self.next_char();
309+
let tok_end = self.get_pos();
310+
return Err(LexicalError {
311+
error: LexicalErrorType::InvalidAssignmentShorthand,
312+
location: SrcSpan {
313+
start: tok_start,
314+
end: tok_end,
315+
},
316+
});
317+
}
318+
_ => {
319+
let tok_end = self.get_pos();
320+
self.emit((tok_start, Token::Percent, tok_end));
321+
}
322+
}
269323
}
270324
'|' => {
271325
let tok_start = self.get_pos();
272326
let _ = self.next_char();
273-
if let Some('|') = self.chr0 {
274-
let _ = self.next_char();
275-
let tok_end = self.get_pos();
276-
self.emit((tok_start, Token::VbarVbar, tok_end));
277-
} else if let Some('>') = self.chr0 {
278-
let _ = self.next_char();
279-
let tok_end = self.get_pos();
280-
self.emit((tok_start, Token::Pipe, tok_end));
281-
} else {
282-
let tok_end = self.get_pos();
283-
self.emit((tok_start, Token::Vbar, tok_end));
327+
match self.chr0 {
328+
Some('|') => {
329+
let _ = self.next_char();
330+
let tok_end = self.get_pos();
331+
self.emit((tok_start, Token::VbarVbar, tok_end));
332+
}
333+
Some('>') => {
334+
let _ = self.next_char();
335+
let tok_end = self.get_pos();
336+
self.emit((tok_start, Token::Pipe, tok_end));
337+
}
338+
_ => {
339+
let tok_end = self.get_pos();
340+
self.emit((tok_start, Token::Vbar, tok_end));
341+
}
284342
}
285343
}
286344
'&' => {
287345
let tok_start = self.get_pos();
288346
let _ = self.next_char();
289-
if let Some('&') = self.chr0 {
290-
let _ = self.next_char();
291-
let tok_end = self.get_pos();
292-
self.emit((tok_start, Token::AmperAmper, tok_end));
293-
} else {
294-
return Err(LexicalError {
295-
error: LexicalErrorType::UnrecognizedToken { tok: '&' },
296-
location: SrcSpan {
297-
start: tok_start,
298-
end: tok_start,
299-
},
300-
});
347+
match self.chr0 {
348+
Some('&') => {
349+
let _ = self.next_char();
350+
let tok_end = self.get_pos();
351+
self.emit((tok_start, Token::AmperAmper, tok_end));
352+
}
353+
_ => {
354+
return Err(LexicalError {
355+
error: LexicalErrorType::UnrecognizedToken { tok: '&' },
356+
location: SrcSpan {
357+
start: tok_start,
358+
end: tok_start,
359+
},
360+
});
361+
}
301362
}
302363
}
303364
'-' => {
@@ -314,6 +375,19 @@ where
314375
let tok_end = self.get_pos();
315376
self.emit((tok_start, Token::RArrow, tok_end));
316377
}
378+
// Not including `-` here because repeated int negation is
379+
// valid gleam
380+
Some('=') => {
381+
let _ = self.next_char();
382+
let tok_end = self.get_pos();
383+
return Err(LexicalError {
384+
error: LexicalErrorType::InvalidAssignmentShorthand,
385+
location: SrcSpan {
386+
start: tok_start,
387+
end: tok_end,
388+
},
389+
});
390+
}
317391
_ => {
318392
let tok_end = self.get_pos();
319393
self.emit((tok_start, Token::Minus, tok_end));

compiler-core/src/parse/tests.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,108 @@ fn triple_equals_with_whitespace() {
354354
);
355355
}
356356

357+
#[test]
358+
fn invalid_assignment_shorthand_plus_equals() {
359+
assert_error!(
360+
"let wobble: Int = 32
361+
wobble += 1",
362+
ParseError {
363+
error: ParseErrorType::LexError {
364+
error: LexicalError {
365+
error: LexicalErrorType::InvalidAssignmentShorthand,
366+
location: SrcSpan { start: 36, end: 38 },
367+
},
368+
},
369+
location: SrcSpan { start: 36, end: 38 },
370+
}
371+
);
372+
}
373+
374+
#[test]
375+
fn invalid_assignment_shorthand_plus_plus() {
376+
assert_error!(
377+
"let wobble: Int = 32
378+
wobble++",
379+
ParseError {
380+
error: ParseErrorType::LexError {
381+
error: LexicalError {
382+
error: LexicalErrorType::InvalidAssignmentShorthand,
383+
location: SrcSpan { start: 35, end: 37 },
384+
},
385+
},
386+
location: SrcSpan { start: 35, end: 37 },
387+
}
388+
);
389+
}
390+
391+
#[test]
392+
fn invalid_assignment_shorthand_minus_equals() {
393+
assert_error!(
394+
"let wobble: Int = 32
395+
wobble -= 1",
396+
ParseError {
397+
error: ParseErrorType::LexError {
398+
error: LexicalError {
399+
error: LexicalErrorType::InvalidAssignmentShorthand,
400+
location: SrcSpan { start: 36, end: 38 },
401+
},
402+
},
403+
location: SrcSpan { start: 36, end: 38 },
404+
}
405+
);
406+
}
407+
408+
#[test]
409+
fn invalid_assignment_shorthand_times_equals() {
410+
assert_error!(
411+
"let wobble: Int = 32
412+
wobble *= 1",
413+
ParseError {
414+
error: ParseErrorType::LexError {
415+
error: LexicalError {
416+
error: LexicalErrorType::InvalidAssignmentShorthand,
417+
location: SrcSpan { start: 36, end: 38 },
418+
},
419+
},
420+
location: SrcSpan { start: 36, end: 38 },
421+
}
422+
);
423+
}
424+
425+
#[test]
426+
fn invalid_assignment_shorthand_divide_equals() {
427+
assert_error!(
428+
"let wobble: Int = 32
429+
wobble /= 1",
430+
ParseError {
431+
error: ParseErrorType::LexError {
432+
error: LexicalError {
433+
error: LexicalErrorType::InvalidAssignmentShorthand,
434+
location: SrcSpan { start: 36, end: 38 },
435+
},
436+
},
437+
location: SrcSpan { start: 36, end: 38 },
438+
}
439+
);
440+
}
441+
442+
#[test]
443+
fn invalid_assignment_shorthand_modulo_equals() {
444+
assert_error!(
445+
"let wobble: Int = 32
446+
wobble %= 1",
447+
ParseError {
448+
error: ParseErrorType::LexError {
449+
error: LexicalError {
450+
error: LexicalErrorType::InvalidAssignmentShorthand,
451+
location: SrcSpan { start: 36, end: 38 },
452+
},
453+
},
454+
location: SrcSpan { start: 36, end: 38 },
455+
}
456+
);
457+
}
458+
357459
// https://github.com/gleam-lang/gleam/issues/1231
358460
#[test]
359461
fn pointless_spread() {

0 commit comments

Comments
 (0)