diff --git a/CHANGELOG.md b/CHANGELOG.md index e33c951c..b8a582ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project does not currently adhere to a particular versioning scheme. - Change tuple syntax to not require parentheses in some cases. ([#554][gh-554]) - Improve error messages in branching statements. ([#464][gh-464]) - Change branches to support ending with ask statements. ([#629][gh-629]) +- Improve hexadecimal and binary floating numbers. ([#648][gh-648]) ## [0.2.36] - 2024-07-04 @@ -414,4 +415,5 @@ and this project does not currently adhere to a particular versioning scheme. [gh-629]: https://github.com/HigherOrderCO/Bend/issues/629 [gh-642]: https://github.com/HigherOrderCO/Bend/issues/642 [gh-643]: https://github.com/HigherOrderCO/Bend/issues/643 +[gh-648]: https://github.com/HigherOrderCO/Bend/issues/648 [Unreleased]: https://github.com/HigherOrderCO/Bend/compare/0.2.36...HEAD diff --git a/docs/syntax.md b/docs/syntax.md index d209b6f7..27c87fea 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -580,6 +580,18 @@ Currently, the 3 number types cannot be mixed. | Bitwise Or | x \| y | int, uint | | Bitwise Xor | x ^ y | int, uint | +Hexadecimal and binary floating-point literals are also supported. + +In these representations, each digit after the point is divided according to the base’s power of the digit's position. +Specifically, for hexadecimal floating-point numbers, each place after the dot represents a fraction of 16 to the power of the digit's depth. +Similarly, for binary floating-point numbers, each place after the dot represents a fraction of 2 to the power of the digit's depth. + +```python +0xA.A == 10.625 + +0b111.111 == 7.875 +``` + ### Constructor Literals Constructors are just functions. @@ -1141,6 +1153,18 @@ Currently, the 3 number types cannot be mixed. | Bitwise Or | (\| x y) | int, uint | | Bitwise Xor | (^ x y) | int, uint | +Hexadecimal and binary floating-point literals are also supported. + +In these representations, each digit after the point is divided according to the base’s power of the digit's position. +Specifically, for hexadecimal floating-point numbers, each place after the dot represents a fraction of 16 to the negative power of the digit's depth. +Similarly, for binary floating-point numbers, each place after the dot represents a fraction of 2 to the negative power of the digit's depth. + +```python +(== 0xA.A 10.625) + +(== 0b111.111 7.875) +``` + ### Character Literal ```rust @@ -1240,14 +1264,17 @@ changed or optimized by the compiler. # Import Syntax ### Import Relative to the File + Paths starting with `./` or `../` are imported relative to the file. ### Import Relative to the Main Folder + Paths that do not start with `./` or `../` are relative to the folder of the main file. ## Syntax ### Import Specific Names from a File, or Files from a Folder + ```py from path import name from path import (name1, name2) @@ -1255,11 +1282,13 @@ import (path/name1, path/name2) ``` ### Import All Names from a File, or All Files from a Folder + ```py from path import * ``` ### Aliasing Imports + ```py from path import name as alias from path import (name1 as Alias1, name2 as Alias2) diff --git a/src/fun/parser.rs b/src/fun/parser.rs index 904c9404..3046070f 100644 --- a/src/fun/parser.rs +++ b/src/fun/parser.rs @@ -1518,80 +1518,85 @@ pub trait ParserCommons<'a>: Parser<'a> { let radix = match self.peek_many(2) { Some("0x") => { self.advance_many(2); - 16 + Radix::Hex } Some("0b") => { self.advance_many(2); - 2 + Radix::Bin } - _ => 10, + _ => Radix::Dec, }; - let num_str = self.take_while(move |c| c.is_digit(radix) || c == '_'); + let num_str = self.take_while(move |c| c.is_digit(radix as u32) || c == '_'); let num_str = num_str.chars().filter(|c| *c != '_').collect::(); let next_is_hex = self.peek_one().map_or(false, |c| "0123456789abcdefABCDEF".contains(c)); if next_is_hex || num_str.is_empty() { - let base = match radix { - 16 => "hexadecimal", - 10 => "decimal", - 2 => "binary", - _ => unreachable!(), - }; - self.expected(format!("valid {base} digit").as_str()) + self.expected(format!("valid {radix} digit").as_str()) } else { - u32::from_str_radix(&num_str, radix).map_err(|e| e.to_string()) + u32::from_str_radix(&num_str, radix as u32).map_err(|e| e.to_string()) + } + } + + fn u32_with_radix(&mut self, radix: Radix) -> ParseResult { + let num_str = self.take_while(move |c| c.is_digit(radix as u32) || c == '_'); + let num_str = num_str.chars().filter(|c| *c != '_').collect::(); + let next_is_hex = self.peek_one().map_or(false, |c| "0123456789abcdefABCDEF".contains(c)); + if next_is_hex || num_str.is_empty() { + self.expected(format!("valid {radix} digit").as_str()) + } else { + u32::from_str_radix(&num_str, radix as u32).map_err(|e| e.to_string()) } } fn parse_number(&mut self) -> ParseResult { let ini_idx = *self.index(); - - // Parses sign - let sgn = if self.try_consume_exactly("+") { + let sign = if self.try_consume_exactly("+") { Some(1) } else if self.try_consume_exactly("-") { Some(-1) } else { None }; - - // Parses main value - let num = self.parse_u32()?; - - // Parses frac value (Float type) - // TODO: Will lead to some rounding errors - // TODO: Doesn't cover very large/small numbers - let fra = if let Some('.') = self.peek_one() { + let radix = match self.peek_many(2) { + Some("0x") => { + self.advance_many(2); + Radix::Hex + } + Some("0b") => { + self.advance_many(2); + Radix::Bin + } + _ => Radix::Dec, + }; + let num = self.u32_with_radix(radix)?; + let frac = if let Some('.') = self.peek_one() { self.advance_one(); - let ini_idx = *self.index(); - let fra = self.parse_u32()? as f32; - let end_idx = *self.index(); - let fra = fra / 10f32.powi((end_idx - ini_idx) as i32); + let fra_str = self.take_while(|c| c.is_digit(radix as u32) || c == '_'); + let fra_str = fra_str.chars().filter(|c| *c != '_').collect::(); + let fra = u32::from_str_radix(&fra_str, radix as u32).map_err(|e| e.to_string())?; + let fra = fra as f32 / (radix.to_f32()).powi(fra_str.len() as i32); Some(fra) } else { None }; - // F24 - if let Some(fra) = fra { - let sgn = sgn.unwrap_or(1); - return Ok(Num::F24(sgn as f32 * (num as f32 + fra))); + if let Some(frac) = frac { + let sign = sign.unwrap_or(1); + return Ok(Num::F24(sign as f32 * (num as f32 + frac))); } - // I24 - if let Some(sgn) = sgn { - let num = sgn * num as i32; + if let Some(sign) = sign { + let num = sign * num as i32; if !(-0x00800000..=0x007fffff).contains(&num) { return self.num_range_err(ini_idx, "I24"); } - return Ok(Num::I24(num)); + Ok(Num::I24(num)) + } else { + if num >= 1 << 24 { + return self.num_range_err(ini_idx, "U24"); + } + Ok(Num::U24(num)) } - - // U24 - if num >= 1 << 24 { - return self.num_range_err(ini_idx, "U24"); - } - Ok(Num::U24(num)) } fn num_range_err(&mut self, ini_idx: usize, typ: &str) -> ParseResult { @@ -1659,3 +1664,30 @@ pub trait ParserCommons<'a>: Parser<'a> { } } } + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Radix { + Bin = 2, + Dec = 10, + Hex = 16, +} + +impl Radix { + fn to_f32(self) -> f32 { + match self { + Radix::Bin => 2., + Radix::Dec => 10., + Radix::Hex => 16., + } + } +} + +impl std::fmt::Display for Radix { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Radix::Bin => write!(f, "binary"), + Radix::Dec => write!(f, "decimal"), + Radix::Hex => write!(f, "hexadecimal"), + } + } +} diff --git a/tests/golden_tests/parse_file/bad_floating.bend b/tests/golden_tests/parse_file/bad_floating.bend new file mode 100644 index 00000000..01171241 --- /dev/null +++ b/tests/golden_tests/parse_file/bad_floating.bend @@ -0,0 +1,2 @@ +def main: + return 0xA.0xA diff --git a/tests/golden_tests/run_file/floating_numbers.bend b/tests/golden_tests/run_file/floating_numbers.bend new file mode 100644 index 00000000..ddf25daf --- /dev/null +++ b/tests/golden_tests/run_file/floating_numbers.bend @@ -0,0 +1,2 @@ +def main: + return [0x12.129, 0x0.2, 0b101.101, 0xAAAAAAAA.AAAAAAAA, 0xA.__A__] diff --git a/tests/snapshots/parse_file__bad_floating.bend.snap b/tests/snapshots/parse_file__bad_floating.bend.snap new file mode 100644 index 00000000..08a1de75 --- /dev/null +++ b/tests/snapshots/parse_file__bad_floating.bend.snap @@ -0,0 +1,9 @@ +--- +source: tests/golden_tests.rs +input_file: tests/golden_tests/parse_file/bad_floating.bend +--- +Errors: +In tests/golden_tests/parse_file/bad_floating.bend : +- expected: newline +- detected: + 2 | return 0xA.0xA diff --git a/tests/snapshots/run_file__floating_numbers.bend.snap b/tests/snapshots/run_file__floating_numbers.bend.snap new file mode 100644 index 00000000..52319155 --- /dev/null +++ b/tests/snapshots/run_file__floating_numbers.bend.snap @@ -0,0 +1,9 @@ +--- +source: tests/golden_tests.rs +input_file: tests/golden_tests/run_file/floating_numbers.bend +--- +NumScott: +[18.072, 0.125, 5.625, 2863333376.000, 10.625] + +Scott: +[18.072, 0.125, 5.625, 2863333376.000, 10.625]