Skip to content

Commit ded474a

Browse files
author
bors-servo
authored
Auto merge of #122 - servo:macros, r=Manishearth
Make match_ignore_ascii_case more efficient, add ascii_case_insensitive_phf_map This improves the performance of `match_ignore_ascii_case!` by replacing calls to `str::eq_ignore_ascii_case` with plain string equality. The input string is converted to ASCII lower-case once, using a stack allocated buffer. A `proc_macro_derive` is used internally to ensure that string patterns are already lower-case, and to computed the size of the buffer (the length of the longest pattern). This should improve runtime performance, it the amount of generated MIR or LLVM IR in release mode still looks proportional to the number of patterns, so this by itself won’t help much with the code bloat in `parse_color_keyword`. To deal with that, this PR also adds a `ascii_case_insensitive_phf_map!` macro that reuses the same stack-allocated buffer mechanism to lower-case an input string, and combines it with a [`phf`](https://github.com/sfackler/rust-phf) static hash map. The two macros are similar but make different treadoffs. PHF probably generates faster and less bloated code, but the map’s values need to be given as a string that contains Rust syntax, due to limitations of procedural macros in current stable Rust. On the other hand, generating a `match` expression allows using control flow statements like `return` or `continue` in its match arms. Fixes #115. <!-- Reviewable:start --> --- This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/rust-cssparser/122) <!-- Reviewable:end -->
2 parents f9282a0 + 136c97a commit ded474a

File tree

6 files changed

+473
-188
lines changed

6 files changed

+473
-188
lines changed

Cargo.toml

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
[package]
22

33
name = "cssparser"
4-
version = "0.10.0"
4+
version = "0.11.0"
55
authors = [ "Simon Sapin <[email protected]>" ]
66

77
description = "Rust implementation of CSS Syntax Level 3"
8-
documentation = "http://servo.github.io/rust-cssparser/cssparser/index.html"
8+
documentation = "https://docs.rs/cssparser/"
99
repository = "https://github.com/servo/rust-cssparser"
1010
readme = "README.md"
1111
keywords = ["css", "syntax", "parser"]
@@ -14,23 +14,25 @@ build = "build.rs"
1414

1515
exclude = ["src/css-parsing-tests"]
1616

17-
[lib]
18-
doctest = false
19-
2017
[dev-dependencies]
2118
rustc-serialize = "0.3"
2219
tempdir = "0.3"
23-
encoding_rs = "0.3.2"
20+
encoding_rs = "0.5"
2421

2522
[dependencies]
23+
cssparser-macros = {path = "./macros", version = "0.1"}
2624
heapsize = {version = "0.3", optional = true}
2725
matches = "0.1"
26+
phf = "0.7"
2827
serde = {version = "0.9", optional = true}
2928

3029
[build-dependencies]
31-
syn = { version = "0.10.6", features = ["full", "visit"]}
30+
syn = "0.11"
3231
quote = "0.3"
3332

3433
[features]
3534
bench = []
3635
dummy_match_byte = []
36+
37+
[workspace]
38+
members = [".", "./macros"]

macros/Cargo.toml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "cssparser-macros"
3+
version = "0.1.0"
4+
authors = ["Simon Sapin <[email protected]>"]
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+
phf_codegen = "0.7"
16+
quote = "0.3"
17+
syn = "0.11"
18+

macros/lib.rs

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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 phf_codegen;
6+
extern crate proc_macro;
7+
#[macro_use] extern crate quote;
8+
extern crate syn;
9+
10+
use std::ascii::AsciiExt;
11+
12+
/// Find a `#[cssparser__assert_ascii_lowercase__data(string = "…", string = "…")]` attribute,
13+
/// and panic if any string contains ASCII uppercase letters.
14+
#[proc_macro_derive(cssparser__assert_ascii_lowercase,
15+
attributes(cssparser__assert_ascii_lowercase__data))]
16+
pub fn assert_ascii_lowercase(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
17+
let input = syn::parse_macro_input(&input.to_string()).unwrap();
18+
let data = list_attr(&input, "cssparser__assert_ascii_lowercase__data");
19+
20+
for sub_attr in data {
21+
let string = sub_attr_value(sub_attr, "string");
22+
assert_eq!(*string, string.to_ascii_lowercase(),
23+
"the expected strings must be given in ASCII lowercase");
24+
}
25+
26+
"".parse().unwrap()
27+
}
28+
29+
/// Find a `#[cssparser__max_len__data(string = "…", string = "…")]` attribute,
30+
/// panic if any string contains ASCII uppercase letters,
31+
/// emit a `MAX_LENGTH` constant with the length of the longest string.
32+
#[proc_macro_derive(cssparser__max_len,
33+
attributes(cssparser__max_len__data))]
34+
pub fn max_len(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
35+
let input = syn::parse_macro_input(&input.to_string()).unwrap();
36+
let data = list_attr(&input, "cssparser__max_len__data");
37+
38+
let lengths = data.iter().map(|sub_attr| sub_attr_value(sub_attr, "string").len());
39+
let max_length = lengths.max().expect("expected at least one string");
40+
41+
let tokens = quote! {
42+
const MAX_LENGTH: usize = #max_length;
43+
};
44+
45+
tokens.as_str().parse().unwrap()
46+
}
47+
48+
/// On `struct $Name($ValueType)`, add a new static method
49+
/// `fn map() -> &'static ::phf::Map<&'static str, $ValueType>`.
50+
/// The map’s content is given as:
51+
/// `#[cssparser__phf_map__kv_pairs(key = "…", value = "…", key = "…", value = "…")]`.
52+
/// Keys are ASCII-lowercased.
53+
#[proc_macro_derive(cssparser__phf_map,
54+
attributes(cssparser__phf_map__kv_pairs))]
55+
pub fn phf_map(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
56+
let input = syn::parse_macro_input(&input.to_string()).unwrap();
57+
let name = &input.ident;
58+
let value_type = match input.body {
59+
syn::Body::Struct(syn::VariantData::Tuple(ref fields)) if fields.len() == 1 => {
60+
&fields[0].ty
61+
}
62+
_ => panic!("expected tuple struct newtype, got {:?}", input.body)
63+
};
64+
65+
let pairs: Vec<_> = list_attr(&input, "cssparser__phf_map__kv_pairs").chunks(2).map(|chunk| {
66+
let key = sub_attr_value(&chunk[0], "key");
67+
let value = sub_attr_value(&chunk[1], "value");
68+
(key.to_ascii_lowercase(), value)
69+
}).collect();
70+
71+
let mut map = phf_codegen::Map::new();
72+
for &(ref key, value) in &pairs {
73+
map.entry(&**key, value);
74+
}
75+
76+
let mut initializer_bytes = Vec::<u8>::new();
77+
let mut initializer_tokens = quote::Tokens::new();
78+
map.build(&mut initializer_bytes).unwrap();
79+
initializer_tokens.append(::std::str::from_utf8(&initializer_bytes).unwrap());
80+
81+
let tokens = quote! {
82+
impl #name {
83+
#[inline]
84+
fn map() -> &'static ::phf::Map<&'static str, #value_type> {
85+
static MAP: ::phf::Map<&'static str, #value_type> = #initializer_tokens;
86+
&MAP
87+
}
88+
}
89+
};
90+
91+
tokens.as_str().parse().unwrap()
92+
}
93+
94+
/// Panic if the first attribute isn’t `#[foo(…)]` with the given name,
95+
/// or return the parameters.
96+
fn list_attr<'a>(input: &'a syn::DeriveInput, expected_name: &str) -> &'a [syn::NestedMetaItem] {
97+
for attr in &input.attrs {
98+
match attr.value {
99+
syn::MetaItem::List(ref name, ref nested) if name == expected_name => {
100+
return nested
101+
}
102+
_ => {}
103+
}
104+
}
105+
panic!("expected a {} attribute", expected_name)
106+
}
107+
108+
/// Panic if `sub_attr` is not a name-value like `foo = "…"` with the given name,
109+
/// or return the value.
110+
fn sub_attr_value<'a>(sub_attr: &'a syn::NestedMetaItem, expected_name: &str) -> &'a str {
111+
match *sub_attr {
112+
syn::NestedMetaItem::MetaItem(
113+
syn::MetaItem::NameValue(ref name, syn::Lit::Str(ref value, _))
114+
)
115+
if name == expected_name => {
116+
value
117+
}
118+
_ => {
119+
panic!("expected a `{} = \"\"` parameter to the attribute, got {:?}",
120+
expected_name, sub_attr)
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)