Skip to content

Commit 291d79c

Browse files
author
bors-servo
authored
Auto merge of #123 - servo:proc-macro-hack, r=nox
Rewrite procedural macro based on the enum discriminant trick We can improve on this with an idea from @dtolnay: ```rust enum Dummy { Input = (0, stringify!( ... )).0 } ``` Instead of a custom attribute, we use an enum with an explicit discriminant (because that’s the only case where a type definition can contain a (const) expression), together with accessing the first field of a literal tuple to ignore other fields. Instead of just string literals, we can accept arbitrary tokens that `stringify!` will later be expanded into something parseable in a const expression context. (`stringify!` doesn’t help in attributes because it is not yet expanded by the time anything other than string literals is rejected.) The details of this hack are hidden away into a new crate, called [`procedural-masquerade`](https://docs.rs/procedural-masquerade/). ---- This enables concrete benefits for users of cssparser: * `match_ignore_ascii_case!` now supports the full syntax of `match` expressions, including alternatives `pattern | other_pattern`, bindings `name @ pattern`, and guards `pattern if condition`. * Map values in `asci_case_insensitive_phf_map!` are now given literally instead of as string literals containing Rust syntax. <!-- 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/123) <!-- Reviewable:end -->
2 parents 4890542 + 4a46a54 commit 291d79c

File tree

12 files changed

+710
-459
lines changed

12 files changed

+710
-459
lines changed

Cargo.toml

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

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

77
description = "Rust implementation of CSS Syntax Level 3"
@@ -20,10 +20,11 @@ tempdir = "0.3"
2020
encoding_rs = "0.5"
2121

2222
[dependencies]
23-
cssparser-macros = {path = "./macros", version = "0.1"}
23+
cssparser-macros = {path = "./macros", version = "0.2"}
2424
heapsize = {version = "0.3", optional = true}
2525
matches = "0.1"
2626
phf = "0.7"
27+
procedural-masquerade = {path = "./procedural-masquerade", version = "0.1"}
2728
serde = {version = "0.9", optional = true}
2829

2930
[build-dependencies]
@@ -35,4 +36,4 @@ bench = []
3536
dummy_match_byte = []
3637

3738
[workspace]
38-
members = [".", "./macros"]
39+
members = [".", "./macros", "./procedural-masquerade"]

build.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,18 @@ mod codegen {
1616
}
1717

1818
#[cfg(not(feature = "dummy_match_byte"))]
19-
#[path = "src/macros/mod.rs"]
20-
mod macros;
19+
#[path = "build/match_byte.rs"]
20+
mod match_byte;
2121

2222
#[cfg(not(feature = "dummy_match_byte"))]
2323
mod codegen {
24-
use macros;
24+
use match_byte;
2525
use std::env;
2626
use std::path::Path;
2727

2828
pub fn main(tokenizer_rs: &Path) {
29-
macros::match_byte::expand(tokenizer_rs,
30-
&Path::new(&env::var("OUT_DIR").unwrap()).join("tokenizer.rs"));
29+
match_byte::expand(tokenizer_rs,
30+
&Path::new(&env::var("OUT_DIR").unwrap()).join("tokenizer.rs"));
3131

3232
}
3333
}
File renamed without changes.

macros/Cargo.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cssparser-macros"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
authors = ["Simon Sapin <[email protected]>"]
55
description = "Procedural macros for cssparser"
66
documentation = "https://docs.rs/cssparser-macros/"
@@ -12,7 +12,7 @@ path = "lib.rs"
1212
proc-macro = true
1313

1414
[dependencies]
15+
procedural-masquerade = {path = "../procedural-masquerade", version = "0.1"}
1516
phf_codegen = "0.7"
16-
quote = "0.3"
17-
syn = "0.11"
18-
17+
quote = "0.3.14"
18+
syn = {version = "0.11.8", features = ["full"]}

macros/lib.rs

+82-99
Original file line numberDiff line numberDiff line change
@@ -2,121 +2,104 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5+
#[macro_use] extern crate procedural_masquerade;
56
extern crate phf_codegen;
67
extern crate proc_macro;
78
#[macro_use] extern crate quote;
89
extern crate syn;
910

1011
use std::ascii::AsciiExt;
1112

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");
13+
define_proc_macros! {
14+
/// Input: the arms of a `match` expression.
15+
///
16+
/// Output: a `MAX_LENGTH` constant with the length of the longest string pattern.
17+
///
18+
/// Panic if the arms contain non-string patterns,
19+
/// or string patterns that contains ASCII uppercase letters.
20+
#[allow(non_snake_case)]
21+
pub fn cssparser_internal__assert_ascii_lowercase__max_len(input: &str) -> String {
22+
let expr = syn::parse_expr(&format!("match x {{ {} }}", input)).unwrap();
23+
let arms = match expr {
24+
syn::Expr { node: syn::ExprKind::Match(_, ref arms), .. } => arms,
25+
_ => panic!("expected a match expression, got {:?}", expr)
26+
};
27+
max_len(arms.iter().flat_map(|arm| &arm.pats).filter_map(|pattern| {
28+
let expr = match *pattern {
29+
syn::Pat::Lit(ref expr) => expr,
30+
syn::Pat::Wild |
31+
syn::Pat::Ident(_, _, None) => return None,
32+
syn::Pat::Ident(_, _, Some(ref sub_pattern)) => {
33+
match **sub_pattern {
34+
syn::Pat::Lit(ref expr) => expr,
35+
syn::Pat::Wild => return None,
36+
_ => panic!("expected string or wildcard pattern, got {:?}", pattern)
37+
}
38+
}
39+
_ => panic!("expected string or wildcard pattern, got {:?}", pattern)
40+
};
41+
match **expr {
42+
syn::Expr { node: syn::ExprKind::Lit(syn::Lit::Str(ref string, _)), .. } => {
43+
assert_eq!(*string, string.to_ascii_lowercase(),
44+
"string patterns must be given in ASCII lowercase");
45+
Some(string.len())
46+
}
47+
_ => panic!("expected string pattern, got {:?}", expr)
48+
}
49+
}))
2450
}
2551

26-
"".parse().unwrap()
27-
}
28-
29-
/// Find a `#[cssparser__max_len__data(string = "…", string = "…")]` attribute,
30-
/// and emit a `MAX_LENGTH` constant with the length of the longest string.
31-
#[proc_macro_derive(cssparser__max_len,
32-
attributes(cssparser__max_len__data))]
33-
pub fn max_len(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
34-
let input = syn::parse_macro_input(&input.to_string()).unwrap();
35-
let data = list_attr(&input, "cssparser__max_len__data");
36-
37-
let lengths = data.iter().map(|sub_attr| sub_attr_value(sub_attr, "string").len());
38-
let max_length = lengths.max().expect("expected at least one string");
39-
40-
let tokens = quote! {
41-
const MAX_LENGTH: usize = #max_length;
42-
};
43-
44-
tokens.as_str().parse().unwrap()
45-
}
46-
47-
/// On `struct $Name($ValueType)`, add a new static method
48-
/// `fn map() -> &'static ::phf::Map<&'static str, $ValueType>`.
49-
/// The map’s content is given as:
50-
/// `#[cssparser__phf_map__kv_pairs(key = "…", value = "…", key = "…", value = "…")]`.
51-
/// Keys are ASCII-lowercased.
52-
#[proc_macro_derive(cssparser__phf_map,
53-
attributes(cssparser__phf_map__kv_pairs))]
54-
pub fn phf_map(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
55-
let input = syn::parse_macro_input(&input.to_string()).unwrap();
56-
let name = &input.ident;
57-
let value_type = match input.body {
58-
syn::Body::Struct(syn::VariantData::Tuple(ref fields)) if fields.len() == 1 => {
59-
&fields[0].ty
60-
}
61-
_ => panic!("expected tuple struct newtype, got {:?}", input.body)
62-
};
63-
64-
let pairs: Vec<_> = list_attr(&input, "cssparser__phf_map__kv_pairs").chunks(2).map(|chunk| {
65-
let key = sub_attr_value(&chunk[0], "key");
66-
let value = sub_attr_value(&chunk[1], "value");
67-
(key.to_ascii_lowercase(), value)
68-
}).collect();
69-
70-
let mut map = phf_codegen::Map::new();
71-
for &(ref key, value) in &pairs {
72-
map.entry(&**key, value);
52+
/// Input: string literals with no separator
53+
///
54+
/// Output: a `MAX_LENGTH` constant with the length of the longest string.
55+
#[allow(non_snake_case)]
56+
pub fn cssparser_internal__max_len(input: &str) -> String {
57+
max_len(syn::parse_token_trees(input).unwrap().iter().map(|tt| string_literal(tt).len()))
7358
}
7459

75-
let mut initializer_bytes = Vec::<u8>::new();
76-
let mut initializer_tokens = quote::Tokens::new();
77-
map.build(&mut initializer_bytes).unwrap();
78-
initializer_tokens.append(::std::str::from_utf8(&initializer_bytes).unwrap());
79-
80-
let tokens = quote! {
81-
impl #name {
82-
#[inline]
83-
fn map() -> &'static ::phf::Map<&'static str, #value_type> {
84-
static MAP: ::phf::Map<&'static str, #value_type> = #initializer_tokens;
85-
&MAP
86-
}
60+
/// Input: parsed as token trees. The first TT is a type. (Can be wrapped in parens.)
61+
/// following TTs are grouped in pairs, each pair being a key as a string literal
62+
/// and the corresponding value as a const expression.
63+
///
64+
/// Output: a rust-phf map, with keys ASCII-lowercased:
65+
/// ```
66+
/// static MAP: &'static ::cssparser::phf::Map<&'static str, $ValueType> = …;
67+
/// ```
68+
#[allow(non_snake_case)]
69+
pub fn cssparser_internal__phf_map(input: &str) -> String {
70+
let token_trees = syn::parse_token_trees(input).unwrap();
71+
let value_type = &token_trees[0];
72+
let pairs: Vec<_> = token_trees[1..].chunks(2).map(|chunk| {
73+
let key = string_literal(&chunk[0]);
74+
let value = &chunk[1];
75+
(key.to_ascii_lowercase(), quote!(#value).to_string())
76+
}).collect();
77+
78+
let mut map = phf_codegen::Map::new();
79+
map.phf_path("::cssparser::_internal__phf");
80+
for &(ref key, ref value) in &pairs {
81+
map.entry(&**key, &**value);
8782
}
88-
};
8983

90-
tokens.as_str().parse().unwrap()
84+
let mut tokens = quote! {
85+
static MAP: ::cssparser::_internal__phf::Map<&'static str, #value_type> =
86+
};
87+
let mut initializer_bytes = Vec::new();
88+
map.build(&mut initializer_bytes).unwrap();
89+
tokens.append(::std::str::from_utf8(&initializer_bytes).unwrap());
90+
tokens.append(";");
91+
tokens.into_string()
92+
}
9193
}
9294

93-
/// Panic if the first attribute isn’t `#[foo(…)]` with the given name,
94-
/// or return the parameters.
95-
fn list_attr<'a>(input: &'a syn::DeriveInput, expected_name: &str) -> &'a [syn::NestedMetaItem] {
96-
for attr in &input.attrs {
97-
match attr.value {
98-
syn::MetaItem::List(ref name, ref nested) if name == expected_name => {
99-
return nested
100-
}
101-
_ => {}
102-
}
103-
}
104-
panic!("expected a {} attribute", expected_name)
95+
fn max_len<I: Iterator<Item=usize>>(lengths: I) -> String {
96+
let max_length = lengths.max().expect("expected at least one string");
97+
quote!( const MAX_LENGTH: usize = #max_length; ).into_string()
10598
}
10699

107-
/// Panic if `sub_attr` is not a name-value like `foo = "…"` with the given name,
108-
/// or return the value.
109-
fn sub_attr_value<'a>(sub_attr: &'a syn::NestedMetaItem, expected_name: &str) -> &'a str {
110-
match *sub_attr {
111-
syn::NestedMetaItem::MetaItem(
112-
syn::MetaItem::NameValue(ref name, syn::Lit::Str(ref value, _))
113-
)
114-
if name == expected_name => {
115-
value
116-
}
117-
_ => {
118-
panic!("expected a `{} = \"\"` parameter to the attribute, got {:?}",
119-
expected_name, sub_attr)
120-
}
100+
fn string_literal(token: &syn::TokenTree) -> &str {
101+
match *token {
102+
syn::TokenTree::Token(syn::Token::Literal(syn::Lit::Str(ref string, _))) => string,
103+
_ => panic!("expected string literal, got {:?}", token)
121104
}
122105
}

procedural-masquerade/Cargo.toml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "procedural-masquerade"
3+
version = "0.1.1"
4+
authors = ["Simon Sapin <[email protected]>"]
5+
description = "macro_rules for making proc_macro_derive pretending to be proc_macro"
6+
documentation = "https://docs.rs/procedural-masquerade/"
7+
repository = "https://github.com/servo/rust-cssparser"
8+
license = "MIT/Apache-2.0"
9+
10+
[lib]
11+
path = "lib.rs"
12+
doctest = false

0 commit comments

Comments
 (0)