pub mod base;
mod error;
use core::{num::ParseFloatError, str};
use base::{NumericalBase, StandardBase};
use num_traits::{CheckedAdd, CheckedMul, Unsigned, Zero};
use crate::{
span::{Span, Spannable, Spanned},
SourceCodeScanner,
};
pub use error::IntegerParseError;
pub use error::ParseNumberResult;
const fn is_numerical(ch: char) -> bool {
ch.is_ascii_alphanumeric() || ch == '_' || ch == '.'
}
const BASE_MAPPINGS: [(&str, StandardBase); 3] = [
("0b", StandardBase::Binary),
("0o", StandardBase::Octal),
("0x", StandardBase::Hexadecimal),
];
fn chop_standard_base(s: &mut &str) -> Option<StandardBase> {
for (prefix, base) in BASE_MAPPINGS {
if s.starts_with(prefix) {
unsafe {
*s = s.get_unchecked(prefix.len()..);
}
return Some(base);
}
}
None
}
fn check_float_suffix(s: &mut &str) -> bool {
let result = s.ends_with('f');
if result {
unsafe { *s = s.get_unchecked(..s.len().unchecked_sub(1)) }
}
result
}
fn parse_integer_from_base<
R: Copy + Zero + CheckedAdd<Output = R> + CheckedMul<Output = R> + From<u8> + Unsigned,
B: NumericalBase,
>(
s: &str,
span: Span,
base: &B,
) -> Spanned<Result<R, IntegerParseError>> {
let mut out = R::zero();
let position = R::from(base.position_value());
for next in s.chars() {
if base.includes(next) {
let value = Into::<R>::into(unsafe { base.value_of_unchecked(next) });
let Some(new_value) = out
.checked_mul(&position)
.and_then(|y| y.checked_add(&value))
else {
return Err(IntegerParseError::OutOfRange).spanned(span);
};
out = new_value;
} else if next != '_' {
return Err(IntegerParseError::InvalidCharacter(next)).spanned(span);
}
}
Ok(out).spanned(span)
}
impl<'src> SourceCodeScanner<'src> {
#[inline]
pub fn consume_standard_number(&self) -> Option<Spanned<&'src str>> {
let first_char = self.peek()?;
if !first_char.is_ascii_digit() && first_char != '.' {
return None;
}
let num = self.capture_str(|| {
self.skip();
let mut sign_allowed = false;
while let Some(char) = self.peek() {
if is_numerical(char) || (sign_allowed && (char == '+' || char == '-')) {
sign_allowed = char == 'e' || char == 'E';
self.skip();
} else {
break;
}
}
});
Some(num)
}
#[inline]
pub fn try_parse_integer_from_base<
R: Copy + Zero + CheckedAdd<Output = R> + CheckedMul<Output = R> + From<u8> + Unsigned,
B: NumericalBase,
>(
&self,
base: &B,
) -> Option<Spanned<Result<R, IntegerParseError>>> {
let Spanned { span, data } = self.try_consume_identifier(
|x| x.is_ascii_alphanumeric() || x == '_',
|x| x.is_ascii_alphanumeric() || x == '_',
)?;
Some(parse_integer_from_base(data, span, base))
}
#[inline]
pub fn try_parse_integer<
R: Copy + Zero + CheckedAdd<Output = R> + CheckedMul<Output = R> + From<u8> + Unsigned,
>(
&self,
) -> Option<Spanned<Result<R, IntegerParseError>>> {
let Spanned { span, data } = self.consume_standard_number()?;
Some(parse_integer_from_base(data, span, &StandardBase::Decimal))
}
#[inline]
pub fn try_parse_float(&self) -> Option<Spanned<Result<f64, ParseFloatError>>> {
let res = self.consume_standard_number()?;
match str::parse::<f64>(&res.data.replace('_', "")) {
Ok(value) => Some(Ok(value).spanned(res.span)),
Err(err) => Some(Err(err).spanned(res.span)),
}
}
#[inline]
pub fn try_parse_number<
R: Copy + Zero + CheckedAdd<Output = R> + CheckedMul<Output = R> + From<u8> + Unsigned,
>(
&self,
) -> Option<Spanned<ParseNumberResult<R>>> {
let parsed_number = self.consume_standard_number()?;
let span = parsed_number.span;
let mut data = parsed_number.data;
if let Some(base) = chop_standard_base(&mut data) {
let result = parse_integer_from_base(data, span, &base);
return Some(ParseNumberResult::Integer(result.data).spanned(result.span));
}
if check_float_suffix(&mut data) || data.contains(['.', '-', '+']) {
return match str::parse::<f64>(&data.replace('_', "")) {
Ok(value) => Some(ParseNumberResult::Float(Ok(value)).spanned(span)),
Err(err) => Some(ParseNumberResult::Float(Err(err)).spanned(span)),
};
}
let result = parse_integer_from_base(data, span, &StandardBase::Decimal);
Some(ParseNumberResult::Integer(result.data).spanned(result.span))
}
}
#[cfg(test)]
mod tests {
use crate::{
common::numeric::{base::StandardBase, IntegerParseError, ParseNumberResult},
span::{Span, Spannable, Spanned},
SourceCodeScanner,
};
fn ctx(code: &str) -> SourceCodeScanner {
SourceCodeScanner::new(code)
}
#[test]
fn consume_standard_number() {
let code = ctx("256 12 h 999");
unsafe {
assert_eq!(
code.consume_standard_number(),
Some("256".spanned(Span::new(0, 3)))
);
code.skip();
assert_eq!(
code.consume_standard_number(),
Some("12".spanned(Span::new(4, 2)))
);
code.skip();
assert_eq!(code.consume_standard_number(), None);
code.skip();
code.skip();
assert_eq!(
code.consume_standard_number(),
Some("999".spanned(Span::new(9, 3)))
);
assert!(!code.has_next());
}
}
#[test]
fn try_parse_integer_from_base() {
let code = ctx("123 10102 % 0x6 300");
unsafe {
assert_eq!(
code.try_parse_integer_from_base(&StandardBase::Decimal),
Some(Ok(123u32).spanned(Span::new(0, 3)))
);
code.skip();
assert_eq!(
code.try_parse_integer_from_base::<u8, _>(&StandardBase::Binary),
Some(Err(IntegerParseError::InvalidCharacter('2')).spanned(Span::new(4, 5)))
);
code.skip();
assert_eq!(
code.try_parse_integer_from_base::<u32, _>(&StandardBase::Hexadecimal),
None
);
code.skip();
code.skip();
assert_eq!(
code.try_parse_integer_from_base::<u8, _>(&StandardBase::Binary),
Some(Err(IntegerParseError::InvalidCharacter('x')).spanned(Span::new(12, 3)))
);
code.skip();
assert_eq!(
code.try_parse_integer_from_base::<u8, _>(&StandardBase::Decimal),
Some(Err(IntegerParseError::OutOfRange).spanned(Span::new(16, 3)))
);
assert_eq!(
code.try_parse_integer_from_base::<u8, _>(&StandardBase::Octal),
None
);
assert!(!code.has_next());
}
}
#[test]
fn try_parse_integer() {
let code = ctx("123 10102 h 0x6 300");
unsafe {
assert_eq!(
code.try_parse_integer(),
Some(Ok(123u32).spanned(Span::new(0, 3)))
);
code.skip();
assert_eq!(
code.try_parse_integer(),
Some(Ok(10102u32).spanned(Span::new(4, 5)))
);
code.skip();
assert_eq!(code.try_parse_integer::<u32>(), None);
code.skip();
code.skip();
assert_eq!(
code.try_parse_integer::<u8>(),
Some(Err(IntegerParseError::InvalidCharacter('x')).spanned(Span::new(12, 3)))
);
code.skip();
assert_eq!(
code.try_parse_integer::<u8>(),
Some(Err(IntegerParseError::OutOfRange).spanned(Span::new(16, 3)))
);
assert_eq!(code.try_parse_integer::<u8>(), None);
assert!(!code.has_next());
}
}
#[test]
fn try_parse_float() {
let code = ctx("123 10.2 h 0x12C .34");
unsafe {
assert_eq!(
code.try_parse_float(),
Some(Ok(123.0).spanned(Span::new(0, 3)))
);
code.skip();
assert_eq!(
code.try_parse_float(),
Some(Ok(10.2).spanned(Span::new(4, 4)))
);
code.skip();
assert_eq!(code.try_parse_float(), None);
code.skip();
code.skip();
let Some(Spanned {
span: _,
data: Err(_),
}) = code.try_parse_float()
else {
panic!("Expected parsed float to exist and return error.");
};
code.skip();
assert_eq!(
code.try_parse_float(),
Some(Ok(0.34).spanned(Span::new(17, 3)))
);
assert_eq!(code.try_parse_float(), None);
assert!(!code.has_next());
}
}
#[test]
fn try_parse_number() {
let code = ctx("12 0x2F 1.25 9f 7E+2");
unsafe {
assert_eq!(
code.try_parse_number::<u32>(),
Some(ParseNumberResult::Integer(Ok(12)).spanned(Span::new(0, 2)))
);
code.skip();
assert_eq!(
code.try_parse_number::<u32>(),
Some(ParseNumberResult::Integer(Ok(0x2F)).spanned(Span::new(3, 4)))
);
code.skip();
assert_eq!(
code.try_parse_number::<u32>(),
Some(ParseNumberResult::Float(Ok(1.25)).spanned(Span::new(8, 4)))
);
code.skip();
assert_eq!(
code.try_parse_number::<u32>(),
Some(ParseNumberResult::Float(Ok(9.0)).spanned(Span::new(13, 2)))
);
code.skip();
assert_eq!(
code.try_parse_number::<u32>(),
Some(ParseNumberResult::Float(Ok(7E+2)).spanned(Span::new(16, 4)))
);
assert_eq!(code.try_parse_number::<u32>(), None);
assert!(!code.has_next());
}
}
}