From 004bcf3cbba9f3d69b6ca1ecb0df91490acf8d73 Mon Sep 17 00:00:00 2001 From: Nicholas Rodrigues Lordello Date: Thu, 14 Sep 2023 21:32:11 +0200 Subject: [PATCH] Const-fn Based Integer Macros --- Cargo.toml | 6 +- macros/Cargo.toml | 16 -- macros/README.md | 16 -- macros/src/lib.rs | 219 ---------------------- macros/src/parse.rs | 431 -------------------------------------------- src/int.rs | 16 +- src/int/parse.rs | 88 +++++++++ src/lib.rs | 10 +- src/parse.rs | 76 +++++++- src/uint.rs | 16 +- src/uint/parse.rs | 58 ++++++ 11 files changed, 255 insertions(+), 697 deletions(-) delete mode 100644 macros/Cargo.toml delete mode 100644 macros/README.md delete mode 100644 macros/src/lib.rs delete mode 100644 macros/src/parse.rs diff --git a/Cargo.toml b/Cargo.toml index f2ab544..4b43e1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,21 +13,19 @@ keywords = ["integer", "u256", "ethereum"] categories = ["cryptography::cryptocurrencies", "mathematics", "no-std"] [package.metadata.docs.rs] -features = ["macros", "serde"] +features = ["serde"] [workspace] members = [ "bench", "fuzz", "intrinsics", - "macros", ] [features] llvm-intrinsics = ["ethnum-intrinsics"] -macros = ["ethnum-macros"] +macros = [] # deprecated [dependencies] ethnum-intrinsics = { version = "=1.2.0", path = "intrinsics", optional = true } -ethnum-macros = { version = "=1.1.0", path = "macros", optional = true } serde = { version = "1", default-features = false, optional = true } diff --git a/macros/Cargo.toml b/macros/Cargo.toml deleted file mode 100644 index 5bd6de2..0000000 --- a/macros/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "ethnum-macros" -version = "1.1.0" -authors = ["Nicholas Rodrigues Lordello "] -edition = "2021" -description = "256-bit integer literals" -documentation = "https://docs.rs/ethnum-macros" -readme = "README.md" -homepage = "https://github.com/nlordell/ethnum-rs" -repository = "https://github.com/nlordell/ethnum-rs" -license = "MIT OR Apache-2.0" -keywords = ["integer", "u256", "ethereum"] -categories = ["cryptography::cryptocurrencies", "mathematics", "no-std"] - -[lib] -proc-macro = true diff --git a/macros/README.md b/macros/README.md deleted file mode 100644 index 59794e5..0000000 --- a/macros/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# `ethnum-macros` - -This crate provides procedural macros for compile-time 256-bit integer literals. - -```rust -assert_eq!(ethnum::int!("42") == 42); -``` - -## Usage - -This is typically not used directly, but instead included with `ethnum`: - -```toml -[dependencies] -ethnum = { version = "*", features = ["macros"] } -``` diff --git a/macros/src/lib.rs b/macros/src/lib.rs deleted file mode 100644 index a0fbd82..0000000 --- a/macros/src/lib.rs +++ /dev/null @@ -1,219 +0,0 @@ -//! Procedural macro for 256-bit integer literals. -//! -//! See [`ethnum::int`](https://docs.rs/ethuint/latest/ethuint/macro.int.html) -//! and [`ethnum::uint`](https://docs.rs/ethnum/latest/ethnum/macro.uint.html) -//! documentation for more information. - -extern crate proc_macro; - -mod parse; - -use self::parse::{Int, LiteralError}; -use parse::Uint; -use proc_macro::{Delimiter, Literal, Span, TokenStream, TokenTree}; - -#[proc_macro] -pub fn int(input: TokenStream) -> TokenStream { - match IntLiteral::generate(input) { - Ok(value) => value.into_tokens(), - Err(err) => err.into_tokens(), - } -} - -#[proc_macro] -pub fn uint(input: TokenStream) -> TokenStream { - match UintLiteral::generate(input) { - Ok(value) => value.into_tokens(), - Err(err) => err.into_tokens(), - } -} - -struct IntLiteral(Int, String); - -impl IntLiteral { - fn generate(input: TokenStream) -> Result { - let input = Input::parse(input)?; - Ok(Self(Int::from_literal(&input.value)?, input.crate_name)) - } - - fn into_tokens(self) -> TokenStream { - let (hi, lo) = self.0.into_words(); - format!("{}::I256::from_words({hi}, {lo})", self.1) - .parse() - .unwrap() - } -} - -struct UintLiteral(Uint, String); - -impl UintLiteral { - fn generate(input: TokenStream) -> Result { - let input = Input::parse(input)?; - Ok(Self(Uint::from_literal(&input.value)?, input.crate_name)) - } - - fn into_tokens(self) -> TokenStream { - let (hi, lo) = self.0.into_words(); - format!("{}::U256::from_words({hi}, {lo})", self.1) - .parse() - .unwrap() - } -} - -struct Input { - value: String, - span: Span, - crate_name: String, -} - -impl Input { - fn parse(input: TokenStream) -> Result { - let mut result = Input { - value: String::new(), - span: Span::call_site(), - crate_name: "::ethnum".to_owned(), - }; - ParserState::start().input(input, &mut result)?.end()?; - - Ok(result) - } -} - -enum ParserState { - String, - CommaOrEof, - Crate, - EqualCrateName, - CrateName, - Eof, -} - -impl ParserState { - fn start() -> Self { - Self::String - } - - fn input(self, input: TokenStream, result: &mut Input) -> Result { - input - .into_iter() - .try_fold(self, |state, token| state.next(token, result)) - } - - fn next(self, token: TokenTree, result: &mut Input) -> Result { - match (&self, &token) { - // Procedural macros invoked from withing `macro_rules!` expansions - // may be grouped with a `Ø` delimiter (which allows operator - // precidence to be preserved). - // - // See - (_, TokenTree::Group(g)) if g.delimiter() == Delimiter::None => { - self.input(g.stream(), result) - } - - (Self::String, TokenTree::Literal(l)) => match parse_string(l) { - Some(value) => { - result.value = value; - result.span = token.span(); - Ok(Self::CommaOrEof) - } - None => Err(self.unexpected(Some(token))), - }, - - (Self::CommaOrEof, TokenTree::Punct(p)) if p.as_char() == ',' => Ok(Self::Crate), - (Self::Crate, TokenTree::Ident(c)) if c.to_string() == "crate" => { - Ok(Self::EqualCrateName) - } - (Self::EqualCrateName, TokenTree::Punct(p)) if p.as_char() == '=' => { - Ok(Self::CrateName) - } - (Self::CrateName, TokenTree::Literal(l)) => match parse_string(l) { - Some(value) => { - result.crate_name = value; - Ok(Self::Eof) - } - None => Err(self.unexpected(Some(token))), - }, - - _ => Err(self.unexpected(Some(token))), - } - } - - fn end(self) -> Result<(), CompileError> { - match self { - Self::CommaOrEof | Self::Eof => Ok(()), - _ => Err(self.unexpected(None)), - } - } - - fn unexpected(self, token: Option) -> CompileError { - let expected = match self { - Self::String => "string literal", - Self::CommaOrEof => "`,` or ", - Self::Crate => "`crate` identifier", - Self::EqualCrateName => "`=`", - Self::CrateName => "crate name string literal", - Self::Eof => "", - }; - let (value, span) = match token { - Some(TokenTree::Group(g)) => { - let delim = match g.delimiter() { - Delimiter::Parenthesis => "(", - Delimiter::Brace => "{", - Delimiter::Bracket => "[", - Delimiter::None => "Ø", - }; - (delim.to_string(), Some(g.span_open())) - } - Some(t) => (t.to_string(), Some(t.span())), - None => ("".to_owned(), None), - }; - - CompileError { - message: format!("expected {expected} but found `{value}`"), - span, - } - } -} - -struct CompileError { - message: String, - span: Option, -} - -impl CompileError { - fn into_tokens(self) -> TokenStream { - let error = format!("compile_error!({:?})", self.message) - .parse::() - .unwrap(); - - match self.span { - Some(span) => error - .into_iter() - .map(|mut token| { - token.set_span(span); - token - }) - .collect(), - None => error, - } - } -} - -impl From for CompileError { - fn from(err: LiteralError) -> Self { - Self { - message: err.to_string(), - span: None, - } - } -} - -fn parse_string(literal: &Literal) -> Option { - Some( - literal - .to_string() - .strip_prefix('"')? - .strip_suffix('"')? - .to_owned(), - ) -} diff --git a/macros/src/parse.rs b/macros/src/parse.rs deleted file mode 100644 index fc56c77..0000000 --- a/macros/src/parse.rs +++ /dev/null @@ -1,431 +0,0 @@ -//! Minimal parsing implementation for 256-bit integers. - -use std::fmt::{self, Display, Formatter}; - -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub struct Int { - hi: i128, - lo: u128, -} - -impl Int { - pub fn into_words(self) -> (i128, i128) { - (self.hi, self.lo as _) - } - - pub fn from_literal(src: &str) -> Result { - from_literal(src) - } - - fn neg(&self) -> Self { - let (lo, carry) = (!self.lo).overflowing_add(1); - let hi = (!self.hi).wrapping_add(carry as _); - - Self { hi, lo } - } - - fn is_negative(&self) -> bool { - self.hi.is_negative() - } - - fn abs(&self) -> (bool, Uint) { - let is_negative = self.is_negative(); - let abs = match is_negative { - true => self.neg(), - false => *self, - }; - - ( - is_negative, - Uint { - hi: abs.hi as _, - lo: abs.lo, - }, - ) - } - - fn new(is_negative: bool, abs: Uint) -> Option { - let mut signed = Int { - hi: abs.hi as _, - lo: abs.lo, - }; - if is_negative { - signed = signed.neg(); - } - if is_negative != signed.is_negative() { - return None; - } - - Some(signed) - } -} - -impl FromLiteral for Int { - fn is_signed() -> bool { - true - } - - fn mul_radix(&self, radix: u32) -> Option { - let (is_negative, abs) = self.abs(); - let result = abs.mul_radix(radix)?; - Self::new(is_negative, result) - } - - fn add_digit(&self, digit: u32) -> Option { - let (lo, carry) = self.lo.overflowing_add(digit as _); - let hi = self.hi.wrapping_add(carry as _); - if (hi, lo) < (self.hi, self.lo) { - return None; - } - - Some(Self { hi, lo }) - } - - fn sub_digit(&self, digit: u32) -> Option { - let (lo, carry) = self.lo.overflowing_sub(digit as _); - let hi = self.hi.wrapping_sub(carry as _); - if (hi, lo) > (self.hi, self.lo) { - return None; - } - - Some(Self { hi, lo }) - } -} - -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub struct Uint { - hi: u128, - lo: u128, -} - -impl Uint { - pub fn into_words(self) -> (u128, u128) { - (self.hi, self.lo) - } - - pub fn from_literal(src: &str) -> Result { - from_literal(src) - } -} - -impl FromLiteral for Uint { - fn is_signed() -> bool { - false - } - - fn mul_radix(&self, radix: u32) -> Option { - let radix = radix as u128; - let (lh, ll) = (self.lo >> 64, self.lo & (u64::MAX as u128)); - - let llx = ll * radix; - let lhx = lh * radix; - let (lo, carry) = llx.overflowing_add(lhx << 64); - - let hlx = (lhx >> 64) + (carry as u128); - let hhx = self.hi.checked_mul(radix)?; - let hi = hhx.checked_add(hlx)?; - - Some(Self { hi, lo }) - } - - fn add_digit(&self, digit: u32) -> Option { - let (lo, carry) = self.lo.overflowing_add(digit as _); - let hi = self.hi.checked_add(carry as _)?; - - Some(Self { hi, lo }) - } - - fn sub_digit(&self, _: u32) -> Option { - None - } -} - -trait FromLiteral: Default { - fn is_signed() -> bool; - fn mul_radix(&self, radix: u32) -> Option; - fn add_digit(&self, digit: u32) -> Option; - fn sub_digit(&self, digit: u32) -> Option; -} - -fn from_literal(src: &str) -> Result { - if src.is_empty() { - return Err(LiteralError::Empty); - } - - let src = src.as_bytes(); - let (is_positive, prefixed_digits) = match src[0] { - b'+' | b'-' if src[1..].is_empty() => { - return Err(LiteralError::InvalidDigit); - } - b'+' => (true, &src[1..]), - b'-' if T::is_signed() => (false, &src[1..]), - _ => (true, src), - }; - - let (radix, digits) = match prefixed_digits.get(..2) { - Some(b"0b") => (2, &prefixed_digits[2..]), - Some(b"0o") => (8, &prefixed_digits[2..]), - Some(b"0x") => (16, &prefixed_digits[2..]), - _ => (10, prefixed_digits), - }; - - type Op = fn(&T, u32) -> Option; - let (op, overflow_err) = match is_positive { - true => (T::add_digit as Op, LiteralError::PosOverflow), - false => (T::sub_digit as Op, LiteralError::NegOverflow), - }; - - let mut result = T::default(); - let mut empty = true; - for &c in digits { - if c == b'_' || c.is_ascii_whitespace() { - // Allow separators and inner-whitespace - continue; - } - - let x = (c as char) - .to_digit(radix) - .ok_or(LiteralError::InvalidDigit)?; - result = result - .mul_radix(radix) - .and_then(|r| op(&r, x)) - .ok_or(overflow_err)?; - empty = false; - } - - if empty { - return Err(LiteralError::InvalidDigit); - } - - Ok(result) -} - -#[derive(Clone, Copy, Debug)] -pub enum LiteralError { - Empty, - InvalidDigit, - PosOverflow, - NegOverflow, -} - -impl Display for LiteralError { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Empty => f.write_str("cannot parse empty integer literal"), - Self::InvalidDigit => f.write_str("invalid digit found in integer literal"), - Self::PosOverflow => f.write_str("integer literal too large"), - Self::NegOverflow => f.write_str("integer literal too small"), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn the_negative_answer() { - let answer = Int::default() - .mul_radix(10) - .unwrap() - .sub_digit(4) - .unwrap() - .mul_radix(10) - .unwrap() - .sub_digit(2) - .unwrap(); - - assert_eq!( - answer, - Int { - hi: -1, - lo: -42 as _, - } - ) - } - - #[test] - fn the_answer() { - let answer = Uint::default() - .mul_radix(10) - .unwrap() - .add_digit(4) - .unwrap() - .mul_radix(10) - .unwrap() - .add_digit(2) - .unwrap(); - - assert_eq!(answer, Uint { hi: 0, lo: 42 }) - } - - #[test] - fn signed_limits() { - for s in [ - "-0b1000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000", - "-0o1000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000", - "-57896044618658097711785492504343953926634992332820282019728792003956564819968", - "-0x8000000000000000000000000000000000000000000000000000000000000000", - ] { - assert_eq!( - Int::from_literal(s).unwrap(), - Int { - hi: i128::MIN, - lo: u128::MIN - }, - ); - } - - for s in ["0b0", "0o0", "0", "0x0"] { - assert_eq!(Int::from_literal(s).unwrap(), Int::default()); - } - - for s in [ - "0b0111111111111111111111111111111111111111111111111111111111111111 - 1111111111111111111111111111111111111111111111111111111111111111 - 1111111111111111111111111111111111111111111111111111111111111111 - 1111111111111111111111111111111111111111111111111111111111111111", - "0o0777777777777777777777777777777777777777777 - 7777777777777777777777777777777777777777777", - "57896044618658097711785492504343953926634992332820282019728792003956564819967", - "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - ] { - assert_eq!( - Int::from_literal(s).unwrap(), - Int { - hi: i128::MAX, - lo: u128::MAX - }, - ); - } - } - - #[test] - fn unsigned_limits() { - for s in ["0b0", "0o0", "0", "0x0"] { - assert_eq!(Uint::from_literal(s).unwrap(), Uint::default()); - } - - for s in [ - "0b1111111111111111111111111111111111111111111111111111111111111111 - 1111111111111111111111111111111111111111111111111111111111111111 - 1111111111111111111111111111111111111111111111111111111111111111 - 1111111111111111111111111111111111111111111111111111111111111111", - "0o1777777777777777777777777777777777777777777 - 7777777777777777777777777777777777777777777", - "115792089237316195423570985008687907853269984665640564039457584007913129639935", - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - ] { - assert_eq!( - Uint::from_literal(s).unwrap(), - Uint { - hi: u128::MAX, - lo: u128::MAX - }, - ); - } - } - - #[test] - fn signed_overflow() { - for s in [ - "-0b1000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000001", - "-0o1000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000001", - "-57896044618658097711785492504343953926634992332820282019728792003956564819969", - "-0x8000000000000000000000000000000000000000000000000000000000000001", - ] { - assert!(matches!( - Int::from_literal(s).unwrap_err(), - LiteralError::NegOverflow, - )); - } - - for s in [ - "0b1000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000", - "0o1000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000", - "57896044618658097711785492504343953926634992332820282019728792003956564819968", - "0x8000000000000000000000000000000000000000000000000000000000000000", - ] { - assert!(matches!( - Int::from_literal(s).unwrap_err(), - LiteralError::PosOverflow, - )); - } - } - - #[test] - fn unsigned_overflow() { - for s in [ - "0b1 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000", - "0o2000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000", - "115792089237316195423570985008687907853269984665640564039457584007913129639936", - "0x1 - 0000000000000000000000000000000000000000000000000000000000000000", - ] { - assert!(matches!( - Uint::from_literal(s).unwrap_err(), - LiteralError::PosOverflow, - )); - } - } - - #[test] - fn separators() { - assert_eq!( - Int::from_literal( - "0b0000000000000000000000000000000000000000000000000000000000000000 - 1010101010101010101010101010101010101010101010101010101010101010 - 0000000000000000000000000000000000000000000000000000000000000000 - 0101010101010101010101010101010101010101010101010101010101010101" - ) - .unwrap(), - Int { - hi: 0b1010101010101010101010101010101010101010101010101010101010101010, - lo: 0b0101010101010101010101010101010101010101010101010101010101010101, - } - ); - - assert_eq!( - Int::from_literal("0o 0123 4567").unwrap(), - Int { - hi: 0, - lo: 0o_0123_4567, - } - ); - - assert_eq!( - Int::from_literal("0xffff_ffff").unwrap(), - Int { - hi: 0, - lo: 0xffff_ffff, - } - ); - } - - #[test] - fn empty_with_separators() { - for s in ["-", "0b _ _", "+_"] { - assert!(matches!( - Int::from_literal(s).unwrap_err(), - LiteralError::InvalidDigit, - )); - } - } -} diff --git a/src/int.rs b/src/int.rs index 1bf3079..2f55be3 100644 --- a/src/int.rs +++ b/src/int.rs @@ -138,9 +138,11 @@ impl I256 { /// /// The string is expected to be an optional `+` or `-` sign followed by /// the one of the supported prefixes and finally the digits. Leading and - /// trailing whitespace represent an error. The base is dertermined based + /// trailing whitespace represent an error. The base is determined based /// on the prefix: /// + /// * `0b`: base `2` + /// * `0o`: base `8` /// * `0x`: base `16` /// * no prefix: base `10` /// @@ -150,13 +152,23 @@ impl I256 { /// /// ``` /// # use ethnum::I256; + /// assert_eq!(I256::from_str_prefixed("-0b101"), Ok(I256::new(-0b101))); + /// assert_eq!(I256::from_str_prefixed("0o17"), Ok(I256::new(0o17))); + /// assert_eq!(I256::from_str_prefixed("-0xa"), Ok(I256::new(-0xa))); /// assert_eq!(I256::from_str_prefixed("42"), Ok(I256::new(42))); - /// assert_eq!(I256::from_str_prefixed("-0xa"), Ok(I256::new(-10))); /// ``` pub fn from_str_prefixed(src: &str) -> Result { crate::parse::from_str_prefixed(src) } + /// Same as [`I256::from_str_prefixed`] but as a `const fn`. This method is + /// not intended to be used directly but rather through the [`crate::int`] + /// macro. + #[doc(hidden)] + pub const fn const_from_str_prefixed(src: &str) -> Self { + parse::const_from_str_prefixed(src) + } + /// Cast to a primitive `i8`. pub const fn as_i8(self) -> i8 { let (_, lo) = self.into_words(); diff --git a/src/int/parse.rs b/src/int/parse.rs index 3e6732d..d8ddecc 100644 --- a/src/int/parse.rs +++ b/src/int/parse.rs @@ -6,6 +6,36 @@ impl_from_str! { impl FromStr for I256; } +pub const fn const_from_str_prefixed(src: &str) -> I256 { + assert!(!src.is_empty(), "empty string"); + + let bytes = src.as_bytes(); + let (negate, start) = match bytes[0] { + b'+' => (false, 1), + b'-' => (true, 1), + _ => (false, 0), + }; + let uint = crate::parse::const_from_str_prefixed(bytes, start as _); + + let int = { + let (hi, lo) = if negate { + let (hi, lo) = uint.into_words(); + let (lo, carry) = (!lo).overflowing_add(1); + let hi = (!hi).wrapping_add(carry as _); + (hi, lo) + } else { + uint.into_words() + }; + I256::from_words(hi as _, lo as _) + }; + + if matches!((negate, int.signum128()), (false, -1) | (true, 1)) { + panic!("overflows integer type"); + } + + int +} + #[cfg(test)] mod tests { use super::*; @@ -60,4 +90,62 @@ mod tests { &IntErrorKind::NegOverflow, ); } + + #[test] + fn const_parse() { + assert_eq!(const_from_str_prefixed("-0b1101"), -0b1101); + assert_eq!(const_from_str_prefixed("0o777"), 0o777); + assert_eq!(const_from_str_prefixed("-0x1f"), -0x1f); + assert_eq!(const_from_str_prefixed("+42"), 42); + + assert_eq!( + const_from_str_prefixed( + "0x7fff_ffff_ffff_ffff_ffff_ffff_ffff_fffe\ + baae_dce6_af48_a03b_bfd2_5e8c_d036_4141" + ), + I256::from_words( + 0x7fff_ffff_ffff_ffff_ffff_ffff_ffff_fffe, + 0xbaae_dce6_af48_a03b_bfd2_5e8c_d036_4141_u128 as _, + ), + ); + + assert_eq!( + const_from_str_prefixed( + "-0x8000_0000_0000_0000_0000_0000_0000_0000\ + 0000_0000_0000_0000_0000_0000_0000_0000" + ), + I256::MIN, + ); + assert_eq!( + const_from_str_prefixed( + "+0x7fff_ffff_ffff_ffff_ffff_ffff_ffff_ffff\ + ffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff" + ), + I256::MAX, + ); + } + + #[test] + #[should_panic] + fn const_parse_overflow() { + const_from_str_prefixed( + "0x8000_0000_0000_0000_0000_0000_0000_0000\ + 0000_0000_0000_0000_0000_0000_0000_0000", + ); + } + + #[test] + #[should_panic] + fn const_parse_negative_overflow() { + const_from_str_prefixed( + "-0x8000_0000_0000_0000_0000_0000_0000_0000\ + 0000_0000_0000_0000_0000_0000_0000_0001", + ); + } + + #[test] + #[should_panic] + fn const_parse_invalid() { + const_from_str_prefixed("invalid"); + } } diff --git a/src/lib.rs b/src/lib.rs index aa7a340..a944298 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,12 +64,11 @@ mod uint; /// assert_eq!(int!("0b101010"), 42); /// assert_eq!(int!("-0o52"), -42); /// ``` -#[cfg(feature = "macros")] #[macro_export] macro_rules! int { ($integer:literal) => {{ - use $crate::internal; - internal::int!($integer, crate = "internal") + const VALUE: $crate::I256 = $crate::I256::const_from_str_prefixed($integer); + VALUE }}; } @@ -107,12 +106,11 @@ macro_rules! int { /// assert_eq!(uint!("0b101010"), 42); /// assert_eq!(uint!("0o52"), 42); /// ``` -#[cfg(feature = "macros")] #[macro_export] macro_rules! uint { ($integer:literal) => {{ - use $crate::internal; - internal::uint!($integer, crate = "internal") + const VALUE: $crate::U256 = $crate::U256::const_from_str_prefixed($integer); + VALUE }}; } diff --git a/src/parse.rs b/src/parse.rs index ed3fb8b..dca813c 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -4,6 +4,7 @@ //! implementation for primitive integer types: //! +use crate::U256; use core::{ mem, num::{IntErrorKind, ParseIntError}, @@ -127,5 +128,78 @@ pub(crate) fn from_str_radix( } pub(crate) fn from_str_prefixed(src: &str) -> Result { - from_str_radix(src, 16, Some("0x")).or_else(|_| from_str_radix(src, 10, None)) + from_str_radix(src, 2, Some("0b")) + .or_else(|_| from_str_radix(src, 8, Some("0o"))) + .or_else(|_| from_str_radix(src, 16, Some("0x"))) + .or_else(|_| from_str_radix(src, 10, None)) +} + +pub(crate) const fn const_from_str_prefixed(bytes: &[u8], start: usize) -> U256 { + const fn check(overflow: bool) { + assert!(!overflow, "overflows integer type"); + } + + const fn add(a: U256, b: u8) -> U256 { + let (hi, lo) = a.into_words(); + + let (lo, carry) = lo.overflowing_add(b as _); + let (hi, overflow) = hi.overflowing_add(carry as _); + check(overflow); + + U256::from_words(hi, lo) + } + + const fn mul(a: U256, r: u128) -> U256 { + let (hi, lo) = a.into_words(); + let (lh, ll) = (lo >> 64, lo & u64::MAX as u128); + + let ll = ll * r; + let lh = lh * r; + let (hi, overflow) = hi.overflowing_mul(r); + check(overflow); + + let (lo, overflow) = ll.overflowing_add(lh << 64); + check(overflow); + let (hi, overflow) = hi.overflowing_add(lh >> 64); + check(overflow); + + U256::from_words(hi, lo) + } + + assert!(bytes.len() > start, "missing number"); + + let (radix, mut i) = if bytes.len() - start > 2 { + match (bytes[start], bytes[start + 1]) { + (b'0', b'b') => (2, start + 2), + (b'0', b'o') => (8, start + 2), + (b'0', b'x') => (16, start + 2), + _ => (10, start), + } + } else { + (10, start) + }; + + let mut value = U256::ZERO; + + while i < bytes.len() { + let byte = bytes[i]; + i += 1; + + if byte == b'_' || byte.is_ascii_whitespace() { + continue; + } + + let next = match (byte, radix) { + (b'0'..=b'1', 2 | 8 | 10 | 16) => byte - b'0', + (b'2'..=b'7', 8 | 10 | 16) => byte - b'0', + (b'8'..=b'9', 10 | 16) => byte - b'0', + (b'a'..=b'f', 16) => byte - b'a' + 0xa, + (b'A'..=b'F', 16) => byte - b'A' + 0xa, + (b'_', _) => continue, + _ => panic!("invalid digit"), + }; + value = add(mul(value, radix), next); + } + + value } diff --git a/src/uint.rs b/src/uint.rs index e064073..3c06870 100644 --- a/src/uint.rs +++ b/src/uint.rs @@ -135,9 +135,11 @@ impl U256 { /// /// The string is expected to be an optional `+` sign followed by the one of /// the supported prefixes and finally the digits. Leading and trailing - /// whitespace represent an error. The base is dertermined based on the + /// whitespace represent an error. The base is determined based on the /// prefix: /// + /// * `0b`: base `2` + /// * `0o`: base `8` /// * `0x`: base `16` /// * no prefix: base `10` /// @@ -147,13 +149,23 @@ impl U256 { /// /// ``` /// # use ethnum::U256; + /// assert_eq!(U256::from_str_prefixed("0b101"), Ok(U256::new(0b101))); + /// assert_eq!(U256::from_str_prefixed("0o17"), Ok(U256::new(0o17))); + /// assert_eq!(U256::from_str_prefixed("0xa"), Ok(U256::new(0xa))); /// assert_eq!(U256::from_str_prefixed("42"), Ok(U256::new(42))); - /// assert_eq!(U256::from_str_prefixed("0xa"), Ok(U256::new(10))); /// ``` pub fn from_str_prefixed(src: &str) -> Result { crate::parse::from_str_prefixed(src) } + /// Same as [`U256::from_str_prefixed`] but as a `const fn`. This method is + /// not intended to be used directly but rather through the [`crate::uint`] + /// macro. + #[doc(hidden)] + pub const fn const_from_str_prefixed(src: &str) -> Self { + parse::const_from_str_prefixed(src) + } + /// Cast to a primitive `i8`. pub const fn as_i8(self) -> i8 { let (_, lo) = self.into_words(); diff --git a/src/uint/parse.rs b/src/uint/parse.rs index efd8f7a..3c4dcbe 100644 --- a/src/uint/parse.rs +++ b/src/uint/parse.rs @@ -6,6 +6,14 @@ impl_from_str! { impl FromStr for U256; } +pub const fn const_from_str_prefixed(src: &str) -> U256 { + assert!(!src.is_empty(), "empty string"); + + let bytes = src.as_bytes(); + let start = bytes[0] == b'+'; + crate::parse::const_from_str_prefixed(bytes, start as _) +} + #[cfg(test)] mod tests { use super::*; @@ -54,4 +62,54 @@ mod tests { &IntErrorKind::PosOverflow, ); } + + #[test] + fn const_parse() { + assert_eq!(const_from_str_prefixed("+0b1101"), 0b1101); + assert_eq!(const_from_str_prefixed("0o777"), 0o777); + assert_eq!(const_from_str_prefixed("+0x1f"), 0x1f); + assert_eq!(const_from_str_prefixed("42"), 42); + + assert_eq!( + const_from_str_prefixed( + "0xffff_ffff_ffff_ffff_ffff_ffff_ffff_fffe\ + baae_dce6_af48_a03b_bfd2_5e8c_d036_4141" + ), + U256::from_words( + 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_fffe, + 0xbaae_dce6_af48_a03b_bfd2_5e8c_d036_4141, + ), + ); + + assert_eq!( + const_from_str_prefixed( + "0x0000_0000_0000_0000_0000_0000_0000_0000\ + 0000_0000_0000_0000_0000_0000_0000_0000" + ), + U256::MIN, + ); + assert_eq!( + const_from_str_prefixed( + "+0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff\ + ffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff" + ), + U256::MAX, + ); + } + + #[test] + #[should_panic] + fn const_parse_overflow() { + const_from_str_prefixed( + "0x1\ + 0000_0000_0000_0000_0000_0000_0000_0000\ + 0000_0000_0000_0000_0000_0000_0000_0000", + ); + } + + #[test] + #[should_panic] + fn const_parse_invalid() { + const_from_str_prefixed("invalid"); + } }