Skip to content

Commit

Permalink
perf: optimize identifier parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
DaniPopes committed Sep 23, 2023
1 parent 3736d33 commit 4219e16
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 30 deletions.
45 changes: 37 additions & 8 deletions crates/dyn-abi/benches/types.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use alloy_dyn_abi::DynSolType;
use alloy_dyn_abi::{DynSolType, ResolveSolType};
use alloy_sol_type_parser::TypeSpecifier;
use criterion::{
criterion_group, criterion_main, measurement::WallTime, BenchmarkGroup, Criterion,
};
use rand::seq::SliceRandom;
use std::{hint::black_box, time::Duration};

static KEYWORDS: &[&str] = &[
const KEYWORDS: &[&str] = &[
"address", "bool", "string", "bytes", "bytes32", "uint", "uint256", "int", "int256",
];
static COMPLEX: &[&str] = &[
const COMPLEX: &[&str] = &[
"((uint104,bytes,bytes8,bytes7,address,bool,address,int256,int32,bytes1,uint56,int136),uint80,uint104,address,bool,bytes14,int16,address,string,uint176,uint72,(uint120,uint192,uint256,int232,bool,bool,bool,bytes5,int56,address,uint224,int248,bytes10,int48,int8),string,string,bool,bool)",
"(address,string,(bytes,int48,bytes30,bool,address,bytes30,int48,address,bytes17,bool,uint32),bool,address,bytes28,bytes25,uint136)",
"(uint168,bytes21,address,(bytes,bool,string,address,bool,string,bytes,uint232,int128,int64,uint96,bytes7,int136),bool,uint200[5],bool,bytes,uint240,address,address,bytes15,bytes)"
Expand All @@ -21,13 +22,43 @@ fn parse(c: &mut Criterion) {
g.bench_function("keywords", |b| {
b.iter(|| {
let kw = *KEYWORDS.choose(rng).unwrap();
DynSolType::parse(black_box(kw)).unwrap()
TypeSpecifier::parse(black_box(kw)).unwrap()
});
});
g.bench_function("complex", |b| {
b.iter(|| {
let complex = *COMPLEX.choose(rng).unwrap();
DynSolType::parse(black_box(complex)).unwrap()
TypeSpecifier::parse(black_box(complex)).unwrap()
});
});

g.finish();
}

fn resolve(c: &mut Criterion) {
let mut g = group(c, "resolve");
let rng = &mut rand::thread_rng();

g.bench_function("keywords", |b| {
let parsed_keywords = KEYWORDS
.iter()
.map(|s| TypeSpecifier::parse(s).unwrap())
.collect::<Vec<_>>();
let parsed_keywords = parsed_keywords.as_slice();
b.iter(|| {
let kw = parsed_keywords.choose(rng).unwrap();
black_box(kw).resolve().unwrap()
});
});
g.bench_function("complex", |b| {
let complex = COMPLEX
.iter()
.map(|s| TypeSpecifier::parse(s).unwrap())
.collect::<Vec<_>>();
let complex = complex.as_slice();
b.iter(|| {
let complex = complex.choose(rng).unwrap();
black_box(complex).resolve().unwrap()
});
});

Expand All @@ -44,7 +75,6 @@ fn format(c: &mut Criterion) {
.map(|s| DynSolType::parse(s).unwrap())
.collect::<Vec<_>>();
let keyword_types = keyword_types.as_slice();
assert!(!keyword_types.is_empty());
b.iter(|| {
let kw = unsafe { keyword_types.choose(rng).unwrap_unchecked() };
black_box(kw).sol_type_name()
Expand All @@ -56,7 +86,6 @@ fn format(c: &mut Criterion) {
.map(|s| DynSolType::parse(s).unwrap())
.collect::<Vec<_>>();
let complex_types = complex_types.as_slice();
assert!(!complex_types.is_empty());
b.iter(|| {
let complex = unsafe { complex_types.choose(rng).unwrap_unchecked() };
black_box(complex).sol_type_name()
Expand All @@ -75,5 +104,5 @@ fn group<'a>(c: &'a mut Criterion, group_name: &str) -> BenchmarkGroup<'a, WallT
g
}

criterion_group!(benches, parse, format);
criterion_group!(benches, parse, resolve, format);
criterion_main!(benches);
2 changes: 1 addition & 1 deletion crates/dyn-abi/src/arbitrary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ mod tests {
tuple,
} => {
prop_assert!(is_valid_identifier(name));
prop_assert!(prop_names.iter().all(is_valid_identifier));
prop_assert!(prop_names.iter().all(|s| is_valid_identifier(s)));
prop_assert_eq!(prop_names.len(), tuple.len());
}
_ => {}
Expand Down
56 changes: 35 additions & 21 deletions crates/sol-type-parser/src/ident.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,40 +21,54 @@ pub const fn is_id_continue(c: char) -> bool {
matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '$')
}

/// Returns `true` if the given string is a valid Solidity identifier.
///
/// An identifier in Solidity has to start with a letter, a dollar-sign or
/// an underscore and may additionally contain numbers after the first
/// symbol.
///
/// Solidity reference:
/// <https://docs.soliditylang.org/en/latest/grammar.html#a4.SolidityLexer.Identifier>
pub fn is_valid_identifier<S: AsRef<str>>(s: S) -> bool {
fn is_valid_identifier(s: &str) -> bool {
let mut chars = s.chars();
if let Some(first) = chars.next() {
is_id_start(first) && chars.all(is_id_continue)
} else {
false
pub const fn is_valid_identifier(s: &str) -> bool {
// Note: valid idents can only contain ASCII characters, so we can
// use the byte representation here.
let [first, rest @ ..] = s.as_bytes() else {
return false
};

if !is_id_start(*first as char) {
return false
}

let mut i = 0;
while i < rest.len() {
if !is_id_continue(rest[i] as char) {
return false
}
i += 1;
}

is_valid_identifier(s.as_ref())
true
}

#[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)
}
}
// See note in `is_valid_identifier` above.
// Use the faster `slice::Iter` instead of `str::Chars`.
let mut chars = input.as_bytes().iter().map(|b| *b as char);

let Some(true) = chars.next().map(is_id_start) else {
return Err(ErrMode::from_error_kind(input, ErrorKind::Fail))
};

// 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..);
Ok(ident)
}
Err(ErrMode::from_error_kind(input, ErrorKind::Fail))
}

#[cfg(test)]
Expand Down
11 changes: 11 additions & 0 deletions crates/sol-type-parser/src/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ impl fmt::Display for RootType<'_> {
}

impl<'a> RootType<'a> {
/// Create a new root type from a string without checking if it's valid.
///
/// # Safety
///
/// The string passed in must be a valid Solidity identifier. See
/// [`is_valid_identifier`].
pub const unsafe fn new_unchecked(s: &'a str) -> Self {
debug_assert!(is_valid_identifier(s));
Self(s)
}

/// Parse a root type from a string.
#[inline]
pub fn parse(input: &'a str) -> Result<Self> {
Expand Down

0 comments on commit 4219e16

Please sign in to comment.