Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: rewrite type parser with winnow #292

Merged
merged 4 commits into from
Sep 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
# Only run tests on latest stable and above
- name: check
if: ${{ matrix.rust == '1.65' }} # MSRV
if: ${{ matrix.flags != '--all-features' && matrix.rust == '1.65' }} # MSRV
run: cargo check --workspace ${{ matrix.flags }}
- name: test
if: ${{ matrix.rust != '1.65' }} # MSRV
Expand Down
8 changes: 4 additions & 4 deletions crates/dyn-abi/benches/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ fn parse(c: &mut Criterion) {

g.bench_function("keywords", |b| {
b.iter(|| {
let kw = KEYWORDS.choose(rng).unwrap();
DynSolType::parse(black_box(*kw)).unwrap()
let kw = *KEYWORDS.choose(rng).unwrap();
DynSolType::parse(black_box(kw)).unwrap()
});
});
g.bench_function("complex", |b| {
b.iter(|| {
let complex = COMPLEX.choose(rng).unwrap();
DynSolType::parse(black_box(*complex)).unwrap()
let complex = *COMPLEX.choose(rng).unwrap();
DynSolType::parse(black_box(complex)).unwrap()
});
});

Expand Down
19 changes: 6 additions & 13 deletions crates/dyn-abi/src/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ impl ResolveSolType for TypeStem<'_> {
}

impl ResolveSolType for TypeSpecifier<'_> {
#[inline]
fn resolve(&self) -> Result<DynSolType> {
self.stem
.resolve()
Expand All @@ -122,12 +121,14 @@ impl ResolveSolType for TypeSpecifier<'_> {
}

impl ResolveSolType for Param {
#[inline]
fn resolve(&self) -> Result<DynSolType> {
resolve_param(&self.ty, &self.components, self.internal_type())
}
}

impl ResolveSolType for EventParam {
#[inline]
fn resolve(&self) -> Result<DynSolType> {
resolve_param(&self.ty, &self.components, self.internal_type())
}
Expand Down Expand Up @@ -198,20 +199,12 @@ mod tests {

#[test]
fn extra_close_parens() {
let test_str = "bool,uint256))";
assert_eq!(
parse(test_str),
Err(alloy_sol_type_parser::Error::invalid_type_string(test_str).into())
);
parse("bool,uint256))").unwrap_err();
}

#[test]
fn extra_open_parents() {
let test_str = "(bool,uint256";
assert_eq!(
parse(test_str),
Err(alloy_sol_type_parser::Error::invalid_type_string(test_str).into())
);
parse("(bool,uint256").unwrap_err();
}

#[test]
Expand Down Expand Up @@ -305,7 +298,7 @@ mod tests {
);

assert_eq!(
parse("tuple(address,bytes, (bool, (string, uint256)[][3]))[2]"),
parse("tuple(address,bytes,(bool,(string,uint256)[][3]))[2]"),
Ok(DynSolType::FixedArray(
Box::new(DynSolType::Tuple(vec![
DynSolType::Address,
Expand Down Expand Up @@ -359,7 +352,7 @@ mod tests {
Ok(())
);
assert_eq!(
TypeSpecifier::try_from("tuple(address,bytes, (bool, (string, uint256)[][3]))[2]")
TypeSpecifier::try_from("tuple(address,bytes,(bool,(string,uint256)[][3]))[2]")
.unwrap()
.try_basic_solidity(),
Ok(())
Expand Down
34 changes: 18 additions & 16 deletions crates/dyn-abi/src/ty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,22 +134,6 @@ impl FromStr for DynSolType {
}

impl DynSolType {
/// Wrap in an array of the specified size
pub(crate) fn array_wrap(self, size: Option<NonZeroUsize>) -> Self {
match size {
Some(size) => Self::FixedArray(Box::new(self), size.get()),
None => Self::Array(Box::new(self)),
}
}

/// Iteratively wrap in arrays.
pub(crate) fn array_wrap_from_iter(
self,
iter: impl IntoIterator<Item = Option<NonZeroUsize>>,
) -> Self {
iter.into_iter().fold(self, Self::array_wrap)
}

/// Parses a Solidity type name string into a [`DynSolType`].
///
/// # Examples
Expand All @@ -169,6 +153,24 @@ impl DynSolType {
.and_then(|t| t.resolve())
}

/// Wrap in an array of the specified size
#[inline]
pub(crate) fn array_wrap(self, size: Option<NonZeroUsize>) -> Self {
match size {
Some(size) => Self::FixedArray(Box::new(self), size.get()),
None => Self::Array(Box::new(self)),
}
}

/// Iteratively wrap in arrays.
#[inline]
pub(crate) fn array_wrap_from_iter(
self,
iter: impl IntoIterator<Item = Option<NonZeroUsize>>,
) -> Self {
iter.into_iter().fold(self, Self::array_wrap)
}

/// Fallible cast to the contents of a variant.
#[inline]
pub fn as_tuple(&self) -> Option<&[Self]> {
Expand Down
6 changes: 4 additions & 2 deletions crates/sol-type-parser/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "alloy-sol-type-parser"
description = "Solidity type specifier string parsing."
description = "Solidity type specifier string parser"
keywords = ["ethereum", "abi", "EVM", "solidity"]
categories = ["no-std", "cryptography::cryptocurrencies"]
homepage = "https://github.com/alloy-rs/core/tree/main/crates/sol-type-parser"
Expand All @@ -18,7 +18,9 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[dependencies]
winnow = { version = "0.5", default-features = false, features = ["alloc"] }

[features]
default = ["std"]
std = []
std = ["winnow/std"]
debug = ["std", "winnow/debug"] # WARNING: Bumps MSRV to at least 1.70
4 changes: 2 additions & 2 deletions crates/sol-type-parser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ assert_eq!(
);

// Type specifiers also work for complex tuples!
let my_tuple = TypeSpecifier::try_from("(uint8, (uint8[], bool))[39]").unwrap();
let my_tuple = TypeSpecifier::try_from("(uint8,(uint8[],bool))[39]").unwrap();
assert_eq!(
my_tuple.stem.span(),
"(uint8, (uint8[], bool))"
"(uint8,(uint8[],bool))"
);

// Types are NOT resolved, so you can parse custom structs just by name.
Expand Down
70 changes: 49 additions & 21 deletions crates/sol-type-parser/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,71 @@
use alloc::string::{String, ToString};
use alloc::string::String;
use core::fmt;

/// Type string parsing result
pub type Result<T> = core::result::Result<T, Error>;
pub type Result<T, E = Error> = core::result::Result<T, E>;

/// A type string parsing error.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
/// Invalid type string, extra chars, or invalid structure.
InvalidTypeString(String),
/// Invalid size for a primitive type (intX, uintX, or bytesX).
InvalidSize(String),
#[derive(Clone, PartialEq, Eq)]
pub struct Error(Repr);

#[cfg(feature = "std")]
impl std::error::Error for Error {}

impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Error").field(&self.0 .0).finish()
}
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}

impl Error {
/// Instantiate a new error.
pub fn new(s: impl fmt::Display) -> Self {
Self::new_("", &s)
}

/// Instantiate a new parser error.
pub(crate) fn parser(e: impl fmt::Display) -> Self {
Self::new_(
if cfg!(feature = "std") {
"parser error:\n"
} else {
"parser error: "
},
&e,
)
}

/// Instantiate an invalid type string error. Invalid type string errors are
/// for type strings that are not valid type strings. E.g. "uint256))))[".
#[inline(always)]
pub fn invalid_type_string(ty: impl ToString) -> Self {
Self::InvalidTypeString(ty.to_string())
pub fn invalid_type_string(ty: impl fmt::Display) -> Self {
Self::new_("invalid type string: ", &ty)
}

/// Instantiate an invalid size error. Invalid size errors are for valid
/// primitive types with invalid sizes. E.g. `"uint7"` or `"bytes1337"` or
/// `"string[aaaaaa]"`.
#[inline(always)]
pub fn invalid_size(ty: impl ToString) -> Self {
Self::InvalidSize(ty.to_string())
pub fn invalid_size(ty: impl fmt::Display) -> Self {
Self::new_("invalid size for type: ", &ty)
}

#[inline(never)]
#[cold]
fn new_(s: &str, e: &dyn fmt::Display) -> Self {
Self(Repr(format!("{s}{e}")))
}
}

#[cfg(feature = "std")]
impl std::error::Error for Error {}
#[derive(Clone, PartialEq, Eq)]
struct Repr(String);

impl fmt::Display for Error {
impl fmt::Display for Repr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidTypeString(s) => write!(f, "Invalid type string: {s}"),
Self::InvalidSize(ty) => write!(f, "Invalid size for type: {ty}"),
}
f.write_str(&self.0)
}
}
65 changes: 65 additions & 0 deletions crates/sol-type-parser/src/ident.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
use winnow::{
error::{ErrMode, ErrorKind, ParserError},
PResult,
};

/// The regular expression for a Solidity identfier.
///
/// <https://docs.soliditylang.org/en/latest/grammar.html#a4.SolidityLexer.Identifier>
Expand Down Expand Up @@ -33,3 +38,63 @@ pub fn is_valid_identifier<S: AsRef<str>>(s: S) -> bool {

is_valid_identifier(s.as_ref())
}

#[inline]
pub(crate) fn parse_identifier<'a>(input: &mut &'a str) -> PResult<&'a str> {
let mut chars = input.chars();
if let Some(first) = chars.next() {
if is_id_start(first) {
// 1 for the first character, we know it's ASCII
let len = 1 + chars.take_while(|c| is_id_continue(*c)).count();
// SAFETY: Only ASCII characters are valid in identifiers
unsafe {
let ident = input.get_unchecked(..len);
*input = input.get_unchecked(len..);
return Ok(ident)
}
}
}
Err(ErrMode::from_error_kind(input, ErrorKind::Fail))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_identifier() {
ident_test("foo", Ok("foo"), "");
ident_test("foo ", Ok("foo"), " ");
ident_test("$foo", Ok("$foo"), "");
ident_test("foo$", Ok("foo$"), "");
ident_test("foo2$", Ok("foo2$"), "");
ident_test("foo 2$", Ok("foo"), " 2$");
ident_test("_foo 2$", Ok("_foo"), " 2$");

ident_test("èfoo", Err(()), "èfoo");
ident_test("fèoo", Ok("f"), "èoo");
ident_test("foèo", Ok("fo"), "èo");
ident_test("fooè", Ok("foo"), "è");

ident_test("3foo", Err(()), "3foo");
ident_test("f3oo", Ok("f3oo"), "");
ident_test("fo3o", Ok("fo3o"), "");
ident_test("foo3", Ok("foo3"), "");
}

#[track_caller]
fn ident_test(mut input: &str, expected: Result<&str, ()>, output: &str) {
assert_eq!(
parse_identifier(&mut input).map_err(drop),
expected,
"result mismatch"
);
if let Ok(expected) = expected {
assert!(
is_valid_identifier(expected),
"expected is not a valid ident"
);
}
assert_eq!(input, output, "output mismatch");
}
}
41 changes: 41 additions & 0 deletions crates/sol-type-parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
#[macro_use]
extern crate alloc;

use core::{slice, str};
use winnow::{
error::{AddContext, ParserError, StrContext, StrContextValue},
trace::trace,
Parser,
};

/// Errors.
mod error;
pub use error::{Error, Result};
Expand All @@ -42,3 +49,37 @@ pub use tuple::TupleSpecifier;
/// Type specifier.
mod type_spec;
pub use type_spec::TypeSpecifier;

#[inline]
pub(crate) fn spanned<'a, O, E>(
mut f: impl Parser<&'a str, O, E>,
) -> impl Parser<&'a str, (&'a str, O), E> {
trace("spanned", move |input: &mut &'a str| {
let start = input.as_ptr();

let mut len = input.len();
let r = f.parse_next(input)?;
len -= input.len();

// SAFETY: str invariant
unsafe {
let span = slice::from_raw_parts(start, len);
debug_assert!(str::from_utf8(span).is_ok());
Ok((str::from_utf8_unchecked(span), r))
}
})
}

#[inline]
pub(crate) fn str_parser<'a, E: ParserError<&'a str> + AddContext<&'a str, StrContext>>(
s: &'static str,
) -> impl Parser<&'a str, &'a str, E> {
#[cfg(feature = "debug")]
let name = format!("str={s:?}");
#[cfg(not(feature = "debug"))]
let name = "str";
trace(
name,
s.context(StrContext::Expected(StrContextValue::StringLiteral(s))),
)
}
Loading
Loading