Skip to content

Commit 808922a

Browse files
committed
Use a proc-macro to optimize match_ignore_ascii_case
Previously, the compiler would emit many `eq_ignore_ascii_case` calls, leading to code bloat and probably some slowness. Now, we pre-lowercase the input in a stack-allocated buffer then match exact strings. Hopefully, the optimizer can turn this into a static table and a loop.
1 parent 0592bea commit 808922a

File tree

4 files changed

+99
-3
lines changed

4 files changed

+99
-3
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ tempdir = "0.3"
2323
encoding_rs = "0.5"
2424

2525
[dependencies]
26+
cssparser-macros = {path = "./macros", version = "0.1"}
2627
heapsize = {version = "0.3", optional = true}
2728
matches = "0.1"
2829
serde = {version = "0.9", optional = true}
@@ -34,3 +35,6 @@ quote = "0.3"
3435
[features]
3536
bench = []
3637
dummy_match_byte = []
38+
39+
[workspace]
40+
members = [".", "./macros"]

macros/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "cssparser-macros"
3+
version = "0.1.0"
4+
authors = ["Simon Sapin <simon.sapin@exyr.org>"]
5+
description = "Procedural macros for cssparser"
6+
documentation = "https://docs.rs/cssparser-macros/"
7+
repository = "https://github.com/servo/rust-cssparser"
8+
license = "MPL-2.0"
9+
10+
[lib]
11+
path = "lib.rs"
12+
proc-macro = true
13+
14+
[dependencies]
15+
syn = "0.11"
16+
quote = "0.3"

macros/lib.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
extern crate proc_macro;
6+
#[macro_use] extern crate quote;
7+
extern crate syn;
8+
9+
use std::ascii::AsciiExt;
10+
11+
#[proc_macro_derive(cssparser__match_ignore_ascii_case__derive,
12+
attributes(cssparser__match_ignore_ascii_case__data))]
13+
pub fn expand_token_stream(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
14+
let input = syn::parse_macro_input(&input.to_string()).unwrap();
15+
16+
let max_length;
17+
18+
match input.attrs[0].value {
19+
syn::MetaItem::List(ref ident, ref nested)
20+
if ident == "cssparser__match_ignore_ascii_case__data" => {
21+
let lengths = nested.iter().map(|sub_attr| match *sub_attr {
22+
syn::NestedMetaItem::MetaItem(
23+
syn::MetaItem::NameValue(ref ident, syn::Lit::Str(ref string, _))
24+
)
25+
if ident == "string" => {
26+
assert_eq!(*string, string.to_ascii_lowercase(),
27+
"the expected strings must be given in ASCII lowercase");
28+
string.len()
29+
}
30+
_ => {
31+
panic!("expected a `string = \"\" parameter to the attribute, got {:?}", sub_attr)
32+
}
33+
});
34+
35+
max_length = lengths.max().expect("expected at least one string")
36+
}
37+
_ => {
38+
panic!("expected a cssparser_match_ignore_ascii_case_data attribute")
39+
}
40+
}
41+
42+
let tokens = quote! {
43+
const MAX_LENGTH: usize = #max_length;
44+
};
45+
46+
tokens.as_str().parse().unwrap()
47+
}

src/lib.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ fn parse_border_spacing(_context: &ParserContext, input: &mut Parser)
6868

6969
#![recursion_limit="200"] // For color::parse_color_keyword
7070

71+
#[macro_use] extern crate cssparser_macros;
7172
#[macro_use] extern crate matches;
7273
#[cfg(test)] extern crate encoding_rs;
7374
#[cfg(test)] extern crate tempdir;
@@ -123,10 +124,16 @@ macro_rules! match_ignore_ascii_case {
123124
// finished parsing
124125
(@inner $value:expr, () -> ($(($string:expr => $result:expr))*) $fallback:expr ) => {
125126
{
126-
use std::ascii::AsciiExt;
127-
match &$value[..] {
127+
#[derive(cssparser__match_ignore_ascii_case__derive)]
128+
#[cssparser__match_ignore_ascii_case__data($(string = $string),+)]
129+
#[allow(dead_code)]
130+
struct Dummy;
131+
132+
// MAX_LENGTH is generated by cssparser_MatchIgnoreAsciiCase_internal
133+
let mut buffer: [u8; MAX_LENGTH] = unsafe { ::std::mem::uninitialized() };
134+
match $crate::_match_ignore_ascii_case__to_lowercase(&mut buffer, &$value[..]) {
128135
$(
129-
s if s.eq_ignore_ascii_case($string) => $result,
136+
Some($string) => $result,
130137
)+
131138
_ => $fallback
132139
}
@@ -139,6 +146,28 @@ macro_rules! match_ignore_ascii_case {
139146
};
140147
}
141148

149+
/// Implementation detail of the match_ignore_ascii_case! macro.
150+
#[doc(hidden)]
151+
#[allow(non_snake_case)]
152+
pub fn _match_ignore_ascii_case__to_lowercase<'a>(buffer: &'a mut [u8], input: &'a str) -> Option<&'a str> {
153+
if let Some(buffer) = buffer.get_mut(..input.len()) {
154+
if let Some(first_uppercase) = input.bytes().position(|byte| matches!(byte, b'A'...b'Z')) {
155+
buffer.copy_from_slice(input.as_bytes());
156+
std::ascii::AsciiExt::make_ascii_lowercase(&mut buffer[first_uppercase..]);
157+
unsafe {
158+
Some(::std::str::from_utf8_unchecked(buffer))
159+
}
160+
} else {
161+
// Input is already lower-case
162+
Some(input)
163+
}
164+
} else {
165+
// Input is longer than buffer, which has the length of the longest expected string:
166+
// none of the expected strings would match.
167+
None
168+
}
169+
}
170+
142171
mod rules_and_declarations;
143172

144173
#[cfg(feature = "dummy_match_byte")]

0 commit comments

Comments
 (0)