From dfe7dd95d04cc49299923b3657b8e5e7efaf17f3 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Sat, 11 Jul 2026 17:37:52 +0200 Subject: [PATCH] allow negative numbers in different bases --- CHANGELOG.md | 4 + compiler-core/src/parse/lexer.rs | 91 +++++++++++++------ ...__tests__parse_negative_binary_number.snap | 81 +++++++++++++++++ ...ts__parse_negative_hexadecimal_number.snap | 81 +++++++++++++++++ ...e__tests__parse_negative_octal_number.snap | 81 +++++++++++++++++ compiler-core/src/parse/tests.rs | 15 +++ 6 files changed, 323 insertions(+), 30 deletions(-) create mode 100644 compiler-core/src/parse/snapshots/gleam_core__parse__tests__parse_negative_binary_number.snap create mode 100644 compiler-core/src/parse/snapshots/gleam_core__parse__tests__parse_negative_hexadecimal_number.snap create mode 100644 compiler-core/src/parse/snapshots/gleam_core__parse__tests__parse_negative_octal_number.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 0720bd29b..f0a9da5d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -555,6 +555,10 @@ when finding the Gleam files of a package. ([Giacomo Cavalieri](https://github.com/giacomocavalieri)) +- Fixed a bug where the compiler would not be able to correctly parse negative + numbers written in binary, octal, or hexadecimal base. + ([Giacomo Cavalieri](https://github.com/giacomocavalieri)) + - The formatter now properly formats binary operations in bit array size segments. ([Andrey Kozhev](https://github.com/ankddev)) diff --git a/compiler-core/src/parse/lexer.rs b/compiler-core/src/parse/lexer.rs index ac59849e4..b45ebf576 100644 --- a/compiler-core/src/parse/lexer.rs +++ b/compiler-core/src/parse/lexer.rs @@ -8,6 +8,7 @@ use crate::parse::LiteralFloatValue; use crate::parse::error::{LexicalError, LexicalErrorType}; use crate::parse::token::Token; use std::char; +use std::ops::Neg; use super::error::InvalidUnicodeEscapeError; @@ -879,27 +880,41 @@ where fn lex_number(&mut self) -> LexResult { let start_pos = self.get_pos(); + + // We call this function after making sure that what comes next starts + // with what seems to be a valid number. If we see that it starts with + // `-` we consume the token and record that the number is negative. + let is_negative = if self.chr0 == Some('-') { + let _ = self.next_char(); + true + } else { + false + }; + let num = if self.chr0 == Some('0') { - if self.chr1 == Some('x') || self.chr1 == Some('X') { - // Hex! - let _ = self.next_char(); - let _ = self.next_char(); - self.lex_number_radix(start_pos, 16, "0x")? - } else if self.chr1 == Some('o') || self.chr1 == Some('O') { - // Octal! - let _ = self.next_char(); - let _ = self.next_char(); - self.lex_number_radix(start_pos, 8, "0o")? - } else if self.chr1 == Some('b') || self.chr1 == Some('B') { - // Binary! - let _ = self.next_char(); - let _ = self.next_char(); - self.lex_number_radix(start_pos, 2, "0b")? - } else { - self.lex_decimal_number()? + match self.chr1 { + Some('x' | 'X') => { + // Hex! + let _ = self.next_char(); + let _ = self.next_char(); + self.lex_number_radix(start_pos, 16, is_negative, "0x")? + } + Some('o' | 'O') => { + // Octal! + let _ = self.next_char(); + let _ = self.next_char(); + self.lex_number_radix(start_pos, 8, is_negative, "0o")? + } + Some('b' | 'B') => { + // Binary! + let _ = self.next_char(); + let _ = self.next_char(); + self.lex_number_radix(start_pos, 2, is_negative, "0b")? + } + _ => self.lex_decimal_number(start_pos, is_negative)?, } } else { - self.lex_decimal_number()? + self.lex_decimal_number(start_pos, is_negative)? }; if Some('_') == self.chr0 { @@ -917,7 +932,13 @@ where } // Lex a hex/octal/decimal/binary number without a decimal point. - fn lex_number_radix(&mut self, start_pos: u32, radix: u32, prefix: &str) -> LexResult { + fn lex_number_radix( + &mut self, + start_pos: u32, + radix: u32, + is_negative: bool, + prefix: &str, + ) -> LexResult { let num = self.radix_run(radix); if num.is_empty() { let location = self.get_pos() - 1; @@ -941,6 +962,13 @@ where let value = format!("{prefix}{num}"); let int_value = super::parse_int_value(&value).expect("int value to parse as bigint"); let end_pos = self.get_pos(); + + let (value, int_value) = if is_negative { + (format!("-{value}"), int_value.neg()) + } else { + (value, int_value) + }; + Ok(( start_pos, Token::Int { @@ -954,21 +982,24 @@ where // Lex a normal number, that is, no octal, hex or binary number. // This function cannot be reached without the head of the stream being either 0-9 or '-', 0-9 - fn lex_decimal_number(&mut self) -> LexResult { - self.lex_decimal_or_int_number(true) + fn lex_decimal_number(&mut self, start_pos: u32, is_negative: bool) -> LexResult { + self.lex_decimal_or_int_number(start_pos, is_negative, true) } - fn lex_int_number(&mut self) -> LexResult { - self.lex_decimal_or_int_number(false) + fn lex_int_number(&mut self, start_pos: u32, is_negative: bool) -> LexResult { + self.lex_decimal_or_int_number(start_pos, is_negative, false) } - fn lex_decimal_or_int_number(&mut self, can_lex_decimal: bool) -> LexResult { - let start_pos = self.get_pos(); + fn lex_decimal_or_int_number( + &mut self, + start_pos: u32, + is_negative: bool, + can_lex_decimal: bool, + ) -> LexResult { let mut value = String::new(); - // consume negative sign - if self.chr0 == Some('-') { - value.push(self.next_char().expect("lex_normal_number negative")); - } + if is_negative { + value.push('-') + }; // consume first run of digits value.push_str(&self.radix_run(10)); @@ -1025,7 +1056,7 @@ where // It can be nested like: `tuple.1.2.3.4` loop { if matches!(self.chr0, Some('0'..='9')) { - let number = self.lex_int_number()?; + let number = self.lex_int_number(self.get_pos(), false)?; self.emit(number); } else { break; diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__parse_negative_binary_number.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__parse_negative_binary_number.snap new file mode 100644 index 000000000..8e70b4267 --- /dev/null +++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__parse_negative_binary_number.snap @@ -0,0 +1,81 @@ +--- +source: compiler-core/src/parse/tests.rs +expression: "pub fn main() { -0b101 }" +--- +Parsed { + module: Module { + name: "", + documentation: [], + type_info: (), + definitions: [ + TargetedDefinition { + definition: Function( + Function { + location: SrcSpan { + start: 0, + end: 13, + }, + body_start: Some( + 14, + ), + end_position: 24, + name: Some( + ( + SrcSpan { + start: 7, + end: 11, + }, + "main", + ), + ), + arguments: [], + body: [ + Expression( + Int { + location: SrcSpan { + start: 16, + end: 22, + }, + value: "-0b101", + int_value: -5, + }, + ), + ], + publicity: Public, + deprecation: NotDeprecated, + return_annotation: None, + return_type: (), + documentation: None, + external_erlang: None, + external_javascript: None, + implementations: Implementations { + gleam: true, + can_run_on_erlang: true, + can_run_on_javascript: true, + uses_erlang_externals: false, + uses_javascript_externals: false, + }, + purity: Pure, + }, + ), + target: None, + }, + ], + names: Names { + local_types: {}, + imported_modules: {}, + type_variables: {}, + local_value_constructors: {}, + reexport_aliases: {}, + }, + unused_definition_positions: {}, + }, + extra: ModuleExtra { + module_comments: [], + doc_comments: [], + comments: [], + empty_lines: [], + new_lines: [], + trailing_commas: [], + }, +} diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__parse_negative_hexadecimal_number.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__parse_negative_hexadecimal_number.snap new file mode 100644 index 000000000..a658a5326 --- /dev/null +++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__parse_negative_hexadecimal_number.snap @@ -0,0 +1,81 @@ +--- +source: compiler-core/src/parse/tests.rs +expression: "pub fn main() { -0x6f }" +--- +Parsed { + module: Module { + name: "", + documentation: [], + type_info: (), + definitions: [ + TargetedDefinition { + definition: Function( + Function { + location: SrcSpan { + start: 0, + end: 13, + }, + body_start: Some( + 14, + ), + end_position: 23, + name: Some( + ( + SrcSpan { + start: 7, + end: 11, + }, + "main", + ), + ), + arguments: [], + body: [ + Expression( + Int { + location: SrcSpan { + start: 16, + end: 21, + }, + value: "-0x6f", + int_value: -111, + }, + ), + ], + publicity: Public, + deprecation: NotDeprecated, + return_annotation: None, + return_type: (), + documentation: None, + external_erlang: None, + external_javascript: None, + implementations: Implementations { + gleam: true, + can_run_on_erlang: true, + can_run_on_javascript: true, + uses_erlang_externals: false, + uses_javascript_externals: false, + }, + purity: Pure, + }, + ), + target: None, + }, + ], + names: Names { + local_types: {}, + imported_modules: {}, + type_variables: {}, + local_value_constructors: {}, + reexport_aliases: {}, + }, + unused_definition_positions: {}, + }, + extra: ModuleExtra { + module_comments: [], + doc_comments: [], + comments: [], + empty_lines: [], + new_lines: [], + trailing_commas: [], + }, +} diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__parse_negative_octal_number.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__parse_negative_octal_number.snap new file mode 100644 index 000000000..4464b4b57 --- /dev/null +++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__parse_negative_octal_number.snap @@ -0,0 +1,81 @@ +--- +source: compiler-core/src/parse/tests.rs +expression: "pub fn main() { -0o157 }" +--- +Parsed { + module: Module { + name: "", + documentation: [], + type_info: (), + definitions: [ + TargetedDefinition { + definition: Function( + Function { + location: SrcSpan { + start: 0, + end: 13, + }, + body_start: Some( + 14, + ), + end_position: 24, + name: Some( + ( + SrcSpan { + start: 7, + end: 11, + }, + "main", + ), + ), + arguments: [], + body: [ + Expression( + Int { + location: SrcSpan { + start: 16, + end: 22, + }, + value: "-0o157", + int_value: -111, + }, + ), + ], + publicity: Public, + deprecation: NotDeprecated, + return_annotation: None, + return_type: (), + documentation: None, + external_erlang: None, + external_javascript: None, + implementations: Implementations { + gleam: true, + can_run_on_erlang: true, + can_run_on_javascript: true, + uses_erlang_externals: false, + uses_javascript_externals: false, + }, + purity: Pure, + }, + ), + target: None, + }, + ], + names: Names { + local_types: {}, + imported_modules: {}, + type_variables: {}, + local_value_constructors: {}, + reexport_aliases: {}, + }, + unused_definition_positions: {}, + }, + extra: ModuleExtra { + module_comments: [], + doc_comments: [], + comments: [], + empty_lines: [], + new_lines: [], + trailing_commas: [], + }, +} diff --git a/compiler-core/src/parse/tests.rs b/compiler-core/src/parse/tests.rs index 579c5e825..5818c6d04 100644 --- a/compiler-core/src/parse/tests.rs +++ b/compiler-core/src/parse/tests.rs @@ -2219,6 +2219,21 @@ const wobble = [..wibble] ); } +#[test] +fn parse_negative_binary_number() { + assert_parse_module!("pub fn main() { -0b101 }"); +} + +#[test] +fn parse_negative_octal_number() { + assert_parse_module!("pub fn main() { -0o157 }"); +} + +#[test] +fn parse_negative_hexadecimal_number() { + assert_parse_module!("pub fn main() { -0x6f }"); +} + #[test] fn append_to_const_list() { assert_module_error!( -- 2.51.2