diff options
Diffstat (limited to 'crates/cfg')
-rw-r--r-- | crates/cfg/Cargo.toml | 1 | ||||
-rw-r--r-- | crates/cfg/src/cfg_expr.rs | 113 | ||||
-rw-r--r-- | crates/cfg/src/dnf.rs | 320 | ||||
-rw-r--r-- | crates/cfg/src/lib.rs | 130 | ||||
-rw-r--r-- | crates/cfg/src/tests.rs | 193 |
5 files changed, 686 insertions, 71 deletions
diff --git a/crates/cfg/Cargo.toml b/crates/cfg/Cargo.toml index a6785ee8e..c68e391c1 100644 --- a/crates/cfg/Cargo.toml +++ b/crates/cfg/Cargo.toml | |||
@@ -17,3 +17,4 @@ tt = { path = "../tt", version = "0.0.0" } | |||
17 | [dev-dependencies] | 17 | [dev-dependencies] |
18 | mbe = { path = "../mbe" } | 18 | mbe = { path = "../mbe" } |
19 | syntax = { path = "../syntax" } | 19 | syntax = { path = "../syntax" } |
20 | expect-test = "1.0" | ||
diff --git a/crates/cfg/src/cfg_expr.rs b/crates/cfg/src/cfg_expr.rs index 336fe25bc..42327f1e1 100644 --- a/crates/cfg/src/cfg_expr.rs +++ b/crates/cfg/src/cfg_expr.rs | |||
@@ -2,30 +2,77 @@ | |||
2 | //! | 2 | //! |
3 | //! See: https://doc.rust-lang.org/reference/conditional-compilation.html#conditional-compilation | 3 | //! See: https://doc.rust-lang.org/reference/conditional-compilation.html#conditional-compilation |
4 | 4 | ||
5 | use std::slice::Iter as SliceIter; | 5 | use std::{fmt, slice::Iter as SliceIter}; |
6 | 6 | ||
7 | use tt::SmolStr; | 7 | use tt::SmolStr; |
8 | 8 | ||
9 | /// A simple configuration value passed in from the outside. | ||
10 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] | ||
11 | pub enum CfgAtom { | ||
12 | /// eg. `#[cfg(test)]` | ||
13 | Flag(SmolStr), | ||
14 | /// eg. `#[cfg(target_os = "linux")]` | ||
15 | /// | ||
16 | /// Note that a key can have multiple values that are all considered "active" at the same time. | ||
17 | /// For example, `#[cfg(target_feature = "sse")]` and `#[cfg(target_feature = "sse2")]`. | ||
18 | KeyValue { key: SmolStr, value: SmolStr }, | ||
19 | } | ||
20 | |||
21 | impl CfgAtom { | ||
22 | /// Returns `true` when the atom comes from the target specification. | ||
23 | /// | ||
24 | /// If this returns `true`, then changing this atom requires changing the compilation target. If | ||
25 | /// it returns `false`, the atom might come from a build script or the build system. | ||
26 | pub fn is_target_defined(&self) -> bool { | ||
27 | match self { | ||
28 | CfgAtom::Flag(flag) => matches!(&**flag, "unix" | "windows"), | ||
29 | CfgAtom::KeyValue { key, value: _ } => matches!( | ||
30 | &**key, | ||
31 | "target_arch" | ||
32 | | "target_os" | ||
33 | | "target_env" | ||
34 | | "target_family" | ||
35 | | "target_endian" | ||
36 | | "target_pointer_width" | ||
37 | | "target_vendor" // NOTE: `target_feature` is left out since it can be configured via `-Ctarget-feature` | ||
38 | ), | ||
39 | } | ||
40 | } | ||
41 | } | ||
42 | |||
43 | impl fmt::Display for CfgAtom { | ||
44 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
45 | match self { | ||
46 | CfgAtom::Flag(name) => write!(f, "{}", name), | ||
47 | CfgAtom::KeyValue { key, value } => write!(f, "{} = {:?}", key, value), | ||
48 | } | ||
49 | } | ||
50 | } | ||
51 | |||
9 | #[derive(Debug, Clone, PartialEq, Eq)] | 52 | #[derive(Debug, Clone, PartialEq, Eq)] |
10 | pub enum CfgExpr { | 53 | pub enum CfgExpr { |
11 | Invalid, | 54 | Invalid, |
12 | Atom(SmolStr), | 55 | Atom(CfgAtom), |
13 | KeyValue { key: SmolStr, value: SmolStr }, | ||
14 | All(Vec<CfgExpr>), | 56 | All(Vec<CfgExpr>), |
15 | Any(Vec<CfgExpr>), | 57 | Any(Vec<CfgExpr>), |
16 | Not(Box<CfgExpr>), | 58 | Not(Box<CfgExpr>), |
17 | } | 59 | } |
18 | 60 | ||
61 | impl From<CfgAtom> for CfgExpr { | ||
62 | fn from(atom: CfgAtom) -> Self { | ||
63 | CfgExpr::Atom(atom) | ||
64 | } | ||
65 | } | ||
66 | |||
19 | impl CfgExpr { | 67 | impl CfgExpr { |
20 | pub fn parse(tt: &tt::Subtree) -> CfgExpr { | 68 | pub fn parse(tt: &tt::Subtree) -> CfgExpr { |
21 | next_cfg_expr(&mut tt.token_trees.iter()).unwrap_or(CfgExpr::Invalid) | 69 | next_cfg_expr(&mut tt.token_trees.iter()).unwrap_or(CfgExpr::Invalid) |
22 | } | 70 | } |
23 | /// Fold the cfg by querying all basic `Atom` and `KeyValue` predicates. | 71 | /// Fold the cfg by querying all basic `Atom` and `KeyValue` predicates. |
24 | pub fn fold(&self, query: &dyn Fn(&SmolStr, Option<&SmolStr>) -> bool) -> Option<bool> { | 72 | pub fn fold(&self, query: &dyn Fn(&CfgAtom) -> bool) -> Option<bool> { |
25 | match self { | 73 | match self { |
26 | CfgExpr::Invalid => None, | 74 | CfgExpr::Invalid => None, |
27 | CfgExpr::Atom(name) => Some(query(name, None)), | 75 | CfgExpr::Atom(atom) => Some(query(atom)), |
28 | CfgExpr::KeyValue { key, value } => Some(query(key, Some(value))), | ||
29 | CfgExpr::All(preds) => { | 76 | CfgExpr::All(preds) => { |
30 | preds.iter().try_fold(true, |s, pred| Some(s && pred.fold(query)?)) | 77 | preds.iter().try_fold(true, |s, pred| Some(s && pred.fold(query)?)) |
31 | } | 78 | } |
@@ -54,7 +101,7 @@ fn next_cfg_expr(it: &mut SliceIter<tt::TokenTree>) -> Option<CfgExpr> { | |||
54 | // FIXME: escape? raw string? | 101 | // FIXME: escape? raw string? |
55 | let value = | 102 | let value = |
56 | SmolStr::new(literal.text.trim_start_matches('"').trim_end_matches('"')); | 103 | SmolStr::new(literal.text.trim_start_matches('"').trim_end_matches('"')); |
57 | CfgExpr::KeyValue { key: name, value } | 104 | CfgAtom::KeyValue { key: name, value }.into() |
58 | } | 105 | } |
59 | _ => return Some(CfgExpr::Invalid), | 106 | _ => return Some(CfgExpr::Invalid), |
60 | } | 107 | } |
@@ -70,7 +117,7 @@ fn next_cfg_expr(it: &mut SliceIter<tt::TokenTree>) -> Option<CfgExpr> { | |||
70 | _ => CfgExpr::Invalid, | 117 | _ => CfgExpr::Invalid, |
71 | } | 118 | } |
72 | } | 119 | } |
73 | _ => CfgExpr::Atom(name), | 120 | _ => CfgAtom::Flag(name).into(), |
74 | }; | 121 | }; |
75 | 122 | ||
76 | // Eat comma separator | 123 | // Eat comma separator |
@@ -81,53 +128,3 @@ fn next_cfg_expr(it: &mut SliceIter<tt::TokenTree>) -> Option<CfgExpr> { | |||
81 | } | 128 | } |
82 | Some(ret) | 129 | Some(ret) |
83 | } | 130 | } |
84 | |||
85 | #[cfg(test)] | ||
86 | mod tests { | ||
87 | use super::*; | ||
88 | |||
89 | use mbe::ast_to_token_tree; | ||
90 | use syntax::ast::{self, AstNode}; | ||
91 | |||
92 | fn assert_parse_result(input: &str, expected: CfgExpr) { | ||
93 | let (tt, _) = { | ||
94 | let source_file = ast::SourceFile::parse(input).ok().unwrap(); | ||
95 | let tt = source_file.syntax().descendants().find_map(ast::TokenTree::cast).unwrap(); | ||
96 | ast_to_token_tree(&tt).unwrap() | ||
97 | }; | ||
98 | let cfg = CfgExpr::parse(&tt); | ||
99 | assert_eq!(cfg, expected); | ||
100 | } | ||
101 | |||
102 | #[test] | ||
103 | fn test_cfg_expr_parser() { | ||
104 | assert_parse_result("#![cfg(foo)]", CfgExpr::Atom("foo".into())); | ||
105 | assert_parse_result("#![cfg(foo,)]", CfgExpr::Atom("foo".into())); | ||
106 | assert_parse_result( | ||
107 | "#![cfg(not(foo))]", | ||
108 | CfgExpr::Not(Box::new(CfgExpr::Atom("foo".into()))), | ||
109 | ); | ||
110 | assert_parse_result("#![cfg(foo(bar))]", CfgExpr::Invalid); | ||
111 | |||
112 | // Only take the first | ||
113 | assert_parse_result(r#"#![cfg(foo, bar = "baz")]"#, CfgExpr::Atom("foo".into())); | ||
114 | |||
115 | assert_parse_result( | ||
116 | r#"#![cfg(all(foo, bar = "baz"))]"#, | ||
117 | CfgExpr::All(vec![ | ||
118 | CfgExpr::Atom("foo".into()), | ||
119 | CfgExpr::KeyValue { key: "bar".into(), value: "baz".into() }, | ||
120 | ]), | ||
121 | ); | ||
122 | |||
123 | assert_parse_result( | ||
124 | r#"#![cfg(any(not(), all(), , bar = "baz",))]"#, | ||
125 | CfgExpr::Any(vec![ | ||
126 | CfgExpr::Not(Box::new(CfgExpr::Invalid)), | ||
127 | CfgExpr::All(vec![]), | ||
128 | CfgExpr::Invalid, | ||
129 | CfgExpr::KeyValue { key: "bar".into(), value: "baz".into() }, | ||
130 | ]), | ||
131 | ); | ||
132 | } | ||
133 | } | ||
diff --git a/crates/cfg/src/dnf.rs b/crates/cfg/src/dnf.rs new file mode 100644 index 000000000..580c9a9a2 --- /dev/null +++ b/crates/cfg/src/dnf.rs | |||
@@ -0,0 +1,320 @@ | |||
1 | //! Disjunctive Normal Form construction. | ||
2 | //! | ||
3 | //! Algorithm from <https://www.cs.drexel.edu/~jjohnson/2015-16/fall/CS270/Lectures/3/dnf.pdf>, | ||
4 | //! which would have been much easier to read if it used pattern matching. It's also missing the | ||
5 | //! entire "distribute ANDs over ORs" part, which is not trivial. Oh well. | ||
6 | //! | ||
7 | //! This is currently both messy and inefficient. Feel free to improve, there are unit tests. | ||
8 | |||
9 | use std::fmt; | ||
10 | |||
11 | use rustc_hash::FxHashSet; | ||
12 | |||
13 | use crate::{CfgAtom, CfgDiff, CfgExpr, CfgOptions, InactiveReason}; | ||
14 | |||
15 | /// A `#[cfg]` directive in Disjunctive Normal Form (DNF). | ||
16 | pub struct DnfExpr { | ||
17 | conjunctions: Vec<Conjunction>, | ||
18 | } | ||
19 | |||
20 | struct Conjunction { | ||
21 | literals: Vec<Literal>, | ||
22 | } | ||
23 | |||
24 | struct Literal { | ||
25 | negate: bool, | ||
26 | var: Option<CfgAtom>, // None = Invalid | ||
27 | } | ||
28 | |||
29 | impl DnfExpr { | ||
30 | pub fn new(expr: CfgExpr) -> Self { | ||
31 | let builder = Builder { expr: DnfExpr { conjunctions: Vec::new() } }; | ||
32 | |||
33 | builder.lower(expr.clone()) | ||
34 | } | ||
35 | |||
36 | /// Computes a list of present or absent atoms in `opts` that cause this expression to evaluate | ||
37 | /// to `false`. | ||
38 | /// | ||
39 | /// Note that flipping a subset of these atoms might be sufficient to make the whole expression | ||
40 | /// evaluate to `true`. For that, see `compute_enable_hints`. | ||
41 | /// | ||
42 | /// Returns `None` when `self` is already true, or contains errors. | ||
43 | pub fn why_inactive(&self, opts: &CfgOptions) -> Option<InactiveReason> { | ||
44 | let mut res = InactiveReason { enabled: Vec::new(), disabled: Vec::new() }; | ||
45 | |||
46 | for conj in &self.conjunctions { | ||
47 | let mut conj_is_true = true; | ||
48 | for lit in &conj.literals { | ||
49 | let atom = lit.var.as_ref()?; | ||
50 | let enabled = opts.enabled.contains(atom); | ||
51 | if lit.negate == enabled { | ||
52 | // Literal is false, but needs to be true for this conjunction. | ||
53 | conj_is_true = false; | ||
54 | |||
55 | if enabled { | ||
56 | res.enabled.push(atom.clone()); | ||
57 | } else { | ||
58 | res.disabled.push(atom.clone()); | ||
59 | } | ||
60 | } | ||
61 | } | ||
62 | |||
63 | if conj_is_true { | ||
64 | // This expression is not actually inactive. | ||
65 | return None; | ||
66 | } | ||
67 | } | ||
68 | |||
69 | res.enabled.sort_unstable(); | ||
70 | res.enabled.dedup(); | ||
71 | res.disabled.sort_unstable(); | ||
72 | res.disabled.dedup(); | ||
73 | Some(res) | ||
74 | } | ||
75 | |||
76 | /// Returns `CfgDiff` objects that would enable this directive if applied to `opts`. | ||
77 | pub fn compute_enable_hints<'a>( | ||
78 | &'a self, | ||
79 | opts: &'a CfgOptions, | ||
80 | ) -> impl Iterator<Item = CfgDiff> + 'a { | ||
81 | // A cfg is enabled if any of `self.conjunctions` evaluate to `true`. | ||
82 | |||
83 | self.conjunctions.iter().filter_map(move |conj| { | ||
84 | let mut enable = FxHashSet::default(); | ||
85 | let mut disable = FxHashSet::default(); | ||
86 | for lit in &conj.literals { | ||
87 | let atom = lit.var.as_ref()?; | ||
88 | let enabled = opts.enabled.contains(atom); | ||
89 | if lit.negate && enabled { | ||
90 | disable.insert(atom.clone()); | ||
91 | } | ||
92 | if !lit.negate && !enabled { | ||
93 | enable.insert(atom.clone()); | ||
94 | } | ||
95 | } | ||
96 | |||
97 | // Check that this actually makes `conj` true. | ||
98 | for lit in &conj.literals { | ||
99 | let atom = lit.var.as_ref()?; | ||
100 | let enabled = enable.contains(atom) | ||
101 | || (opts.enabled.contains(atom) && !disable.contains(atom)); | ||
102 | if enabled == lit.negate { | ||
103 | return None; | ||
104 | } | ||
105 | } | ||
106 | |||
107 | if enable.is_empty() && disable.is_empty() { | ||
108 | return None; | ||
109 | } | ||
110 | |||
111 | let mut diff = CfgDiff { | ||
112 | enable: enable.into_iter().collect(), | ||
113 | disable: disable.into_iter().collect(), | ||
114 | }; | ||
115 | |||
116 | // Undo the FxHashMap randomization for consistent output. | ||
117 | diff.enable.sort_unstable(); | ||
118 | diff.disable.sort_unstable(); | ||
119 | |||
120 | Some(diff) | ||
121 | }) | ||
122 | } | ||
123 | } | ||
124 | |||
125 | impl fmt::Display for DnfExpr { | ||
126 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
127 | if self.conjunctions.len() != 1 { | ||
128 | write!(f, "any(")?; | ||
129 | } | ||
130 | for (i, conj) in self.conjunctions.iter().enumerate() { | ||
131 | if i != 0 { | ||
132 | f.write_str(", ")?; | ||
133 | } | ||
134 | |||
135 | write!(f, "{}", conj)?; | ||
136 | } | ||
137 | if self.conjunctions.len() != 1 { | ||
138 | write!(f, ")")?; | ||
139 | } | ||
140 | |||
141 | Ok(()) | ||
142 | } | ||
143 | } | ||
144 | |||
145 | impl Conjunction { | ||
146 | fn new(parts: Vec<CfgExpr>) -> Self { | ||
147 | let mut literals = Vec::new(); | ||
148 | for part in parts { | ||
149 | match part { | ||
150 | CfgExpr::Invalid | CfgExpr::Atom(_) | CfgExpr::Not(_) => { | ||
151 | literals.push(Literal::new(part)); | ||
152 | } | ||
153 | CfgExpr::All(conj) => { | ||
154 | // Flatten. | ||
155 | literals.extend(Conjunction::new(conj).literals); | ||
156 | } | ||
157 | CfgExpr::Any(_) => unreachable!("disjunction in conjunction"), | ||
158 | } | ||
159 | } | ||
160 | |||
161 | Self { literals } | ||
162 | } | ||
163 | } | ||
164 | |||
165 | impl fmt::Display for Conjunction { | ||
166 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
167 | if self.literals.len() != 1 { | ||
168 | write!(f, "all(")?; | ||
169 | } | ||
170 | for (i, lit) in self.literals.iter().enumerate() { | ||
171 | if i != 0 { | ||
172 | f.write_str(", ")?; | ||
173 | } | ||
174 | |||
175 | write!(f, "{}", lit)?; | ||
176 | } | ||
177 | if self.literals.len() != 1 { | ||
178 | write!(f, ")")?; | ||
179 | } | ||
180 | |||
181 | Ok(()) | ||
182 | } | ||
183 | } | ||
184 | |||
185 | impl Literal { | ||
186 | fn new(expr: CfgExpr) -> Self { | ||
187 | match expr { | ||
188 | CfgExpr::Invalid => Self { negate: false, var: None }, | ||
189 | CfgExpr::Atom(atom) => Self { negate: false, var: Some(atom) }, | ||
190 | CfgExpr::Not(expr) => match *expr { | ||
191 | CfgExpr::Invalid => Self { negate: true, var: None }, | ||
192 | CfgExpr::Atom(atom) => Self { negate: true, var: Some(atom) }, | ||
193 | _ => unreachable!("non-atom {:?}", expr), | ||
194 | }, | ||
195 | CfgExpr::Any(_) | CfgExpr::All(_) => unreachable!("non-literal {:?}", expr), | ||
196 | } | ||
197 | } | ||
198 | } | ||
199 | |||
200 | impl fmt::Display for Literal { | ||
201 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
202 | if self.negate { | ||
203 | write!(f, "not(")?; | ||
204 | } | ||
205 | |||
206 | match &self.var { | ||
207 | Some(var) => write!(f, "{}", var)?, | ||
208 | None => f.write_str("<invalid>")?, | ||
209 | } | ||
210 | |||
211 | if self.negate { | ||
212 | write!(f, ")")?; | ||
213 | } | ||
214 | |||
215 | Ok(()) | ||
216 | } | ||
217 | } | ||
218 | |||
219 | struct Builder { | ||
220 | expr: DnfExpr, | ||
221 | } | ||
222 | |||
223 | impl Builder { | ||
224 | fn lower(mut self, expr: CfgExpr) -> DnfExpr { | ||
225 | let expr = make_nnf(expr); | ||
226 | let expr = make_dnf(expr); | ||
227 | |||
228 | match expr { | ||
229 | CfgExpr::Invalid | CfgExpr::Atom(_) | CfgExpr::Not(_) => { | ||
230 | self.expr.conjunctions.push(Conjunction::new(vec![expr])); | ||
231 | } | ||
232 | CfgExpr::All(conj) => { | ||
233 | self.expr.conjunctions.push(Conjunction::new(conj)); | ||
234 | } | ||
235 | CfgExpr::Any(mut disj) => { | ||
236 | disj.reverse(); | ||
237 | while let Some(conj) = disj.pop() { | ||
238 | match conj { | ||
239 | CfgExpr::Invalid | CfgExpr::Atom(_) | CfgExpr::All(_) | CfgExpr::Not(_) => { | ||
240 | self.expr.conjunctions.push(Conjunction::new(vec![conj])); | ||
241 | } | ||
242 | CfgExpr::Any(inner_disj) => { | ||
243 | // Flatten. | ||
244 | disj.extend(inner_disj.into_iter().rev()); | ||
245 | } | ||
246 | } | ||
247 | } | ||
248 | } | ||
249 | } | ||
250 | |||
251 | self.expr | ||
252 | } | ||
253 | } | ||
254 | |||
255 | fn make_dnf(expr: CfgExpr) -> CfgExpr { | ||
256 | match expr { | ||
257 | CfgExpr::Invalid | CfgExpr::Atom(_) | CfgExpr::Not(_) => expr, | ||
258 | CfgExpr::Any(e) => CfgExpr::Any(e.into_iter().map(|expr| make_dnf(expr)).collect()), | ||
259 | CfgExpr::All(e) => { | ||
260 | let e = e.into_iter().map(|expr| make_nnf(expr)).collect::<Vec<_>>(); | ||
261 | |||
262 | CfgExpr::Any(distribute_conj(&e)) | ||
263 | } | ||
264 | } | ||
265 | } | ||
266 | |||
267 | /// Turns a conjunction of expressions into a disjunction of expressions. | ||
268 | fn distribute_conj(conj: &[CfgExpr]) -> Vec<CfgExpr> { | ||
269 | fn go(out: &mut Vec<CfgExpr>, with: &mut Vec<CfgExpr>, rest: &[CfgExpr]) { | ||
270 | match rest { | ||
271 | [head, tail @ ..] => match head { | ||
272 | CfgExpr::Any(disj) => { | ||
273 | for part in disj { | ||
274 | with.push(part.clone()); | ||
275 | go(out, with, tail); | ||
276 | with.pop(); | ||
277 | } | ||
278 | } | ||
279 | _ => { | ||
280 | with.push(head.clone()); | ||
281 | go(out, with, tail); | ||
282 | with.pop(); | ||
283 | } | ||
284 | }, | ||
285 | _ => { | ||
286 | // Turn accumulated parts into a new conjunction. | ||
287 | out.push(CfgExpr::All(with.clone())); | ||
288 | } | ||
289 | } | ||
290 | } | ||
291 | |||
292 | let mut out = Vec::new(); | ||
293 | let mut with = Vec::new(); | ||
294 | |||
295 | go(&mut out, &mut with, conj); | ||
296 | |||
297 | out | ||
298 | } | ||
299 | |||
300 | fn make_nnf(expr: CfgExpr) -> CfgExpr { | ||
301 | match expr { | ||
302 | CfgExpr::Invalid | CfgExpr::Atom(_) => expr, | ||
303 | CfgExpr::Any(expr) => CfgExpr::Any(expr.into_iter().map(|expr| make_nnf(expr)).collect()), | ||
304 | CfgExpr::All(expr) => CfgExpr::All(expr.into_iter().map(|expr| make_nnf(expr)).collect()), | ||
305 | CfgExpr::Not(operand) => match *operand { | ||
306 | CfgExpr::Invalid | CfgExpr::Atom(_) => CfgExpr::Not(operand.clone()), // Original negated expr | ||
307 | CfgExpr::Not(expr) => { | ||
308 | // Remove double negation. | ||
309 | make_nnf(*expr) | ||
310 | } | ||
311 | // Convert negated conjunction/disjunction using DeMorgan's Law. | ||
312 | CfgExpr::Any(inner) => CfgExpr::All( | ||
313 | inner.into_iter().map(|expr| make_nnf(CfgExpr::Not(Box::new(expr)))).collect(), | ||
314 | ), | ||
315 | CfgExpr::All(inner) => CfgExpr::Any( | ||
316 | inner.into_iter().map(|expr| make_nnf(CfgExpr::Not(Box::new(expr)))).collect(), | ||
317 | ), | ||
318 | }, | ||
319 | } | ||
320 | } | ||
diff --git a/crates/cfg/src/lib.rs b/crates/cfg/src/lib.rs index a9d50e698..d0e08cf5f 100644 --- a/crates/cfg/src/lib.rs +++ b/crates/cfg/src/lib.rs | |||
@@ -1,11 +1,17 @@ | |||
1 | //! cfg defines conditional compiling options, `cfg` attibute parser and evaluator | 1 | //! cfg defines conditional compiling options, `cfg` attibute parser and evaluator |
2 | 2 | ||
3 | mod cfg_expr; | 3 | mod cfg_expr; |
4 | mod dnf; | ||
5 | #[cfg(test)] | ||
6 | mod tests; | ||
7 | |||
8 | use std::fmt; | ||
4 | 9 | ||
5 | use rustc_hash::FxHashSet; | 10 | use rustc_hash::FxHashSet; |
6 | use tt::SmolStr; | 11 | use tt::SmolStr; |
7 | 12 | ||
8 | pub use cfg_expr::CfgExpr; | 13 | pub use cfg_expr::{CfgAtom, CfgExpr}; |
14 | pub use dnf::DnfExpr; | ||
9 | 15 | ||
10 | /// Configuration options used for conditional compilition on items with `cfg` attributes. | 16 | /// Configuration options used for conditional compilition on items with `cfg` attributes. |
11 | /// We have two kind of options in different namespaces: atomic options like `unix`, and | 17 | /// We have two kind of options in different namespaces: atomic options like `unix`, and |
@@ -19,33 +25,131 @@ pub use cfg_expr::CfgExpr; | |||
19 | /// See: https://doc.rust-lang.org/reference/conditional-compilation.html#set-configuration-options | 25 | /// See: https://doc.rust-lang.org/reference/conditional-compilation.html#set-configuration-options |
20 | #[derive(Debug, Clone, PartialEq, Eq, Default)] | 26 | #[derive(Debug, Clone, PartialEq, Eq, Default)] |
21 | pub struct CfgOptions { | 27 | pub struct CfgOptions { |
22 | atoms: FxHashSet<SmolStr>, | 28 | enabled: FxHashSet<CfgAtom>, |
23 | key_values: FxHashSet<(SmolStr, SmolStr)>, | ||
24 | } | 29 | } |
25 | 30 | ||
26 | impl CfgOptions { | 31 | impl CfgOptions { |
27 | pub fn check(&self, cfg: &CfgExpr) -> Option<bool> { | 32 | pub fn check(&self, cfg: &CfgExpr) -> Option<bool> { |
28 | cfg.fold(&|key, value| match value { | 33 | cfg.fold(&|atom| self.enabled.contains(atom)) |
29 | None => self.atoms.contains(key), | ||
30 | Some(value) => self.key_values.contains(&(key.clone(), value.clone())), | ||
31 | }) | ||
32 | } | 34 | } |
33 | 35 | ||
34 | pub fn insert_atom(&mut self, key: SmolStr) { | 36 | pub fn insert_atom(&mut self, key: SmolStr) { |
35 | self.atoms.insert(key); | 37 | self.enabled.insert(CfgAtom::Flag(key)); |
36 | } | 38 | } |
37 | 39 | ||
38 | pub fn insert_key_value(&mut self, key: SmolStr, value: SmolStr) { | 40 | pub fn insert_key_value(&mut self, key: SmolStr, value: SmolStr) { |
39 | self.key_values.insert((key, value)); | 41 | self.enabled.insert(CfgAtom::KeyValue { key, value }); |
40 | } | 42 | } |
41 | 43 | ||
42 | pub fn append(&mut self, other: &CfgOptions) { | 44 | pub fn append(&mut self, other: &CfgOptions) { |
43 | for atom in &other.atoms { | 45 | for atom in &other.enabled { |
44 | self.atoms.insert(atom.clone()); | 46 | self.enabled.insert(atom.clone()); |
47 | } | ||
48 | } | ||
49 | |||
50 | pub fn apply_diff(&mut self, diff: CfgDiff) { | ||
51 | for atom in diff.enable { | ||
52 | self.enabled.insert(atom); | ||
45 | } | 53 | } |
46 | 54 | ||
47 | for (key, value) in &other.key_values { | 55 | for atom in diff.disable { |
48 | self.key_values.insert((key.clone(), value.clone())); | 56 | self.enabled.remove(&atom); |
57 | } | ||
58 | } | ||
59 | } | ||
60 | |||
61 | pub struct CfgDiff { | ||
62 | // Invariants: No duplicates, no atom that's both in `enable` and `disable`. | ||
63 | enable: Vec<CfgAtom>, | ||
64 | disable: Vec<CfgAtom>, | ||
65 | } | ||
66 | |||
67 | impl CfgDiff { | ||
68 | /// Returns the total number of atoms changed by this diff. | ||
69 | pub fn len(&self) -> usize { | ||
70 | self.enable.len() + self.disable.len() | ||
71 | } | ||
72 | } | ||
73 | |||
74 | impl fmt::Display for CfgDiff { | ||
75 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
76 | if !self.enable.is_empty() { | ||
77 | f.write_str("enable ")?; | ||
78 | for (i, atom) in self.enable.iter().enumerate() { | ||
79 | let sep = match i { | ||
80 | 0 => "", | ||
81 | _ if i == self.enable.len() - 1 => " and ", | ||
82 | _ => ", ", | ||
83 | }; | ||
84 | f.write_str(sep)?; | ||
85 | |||
86 | write!(f, "{}", atom)?; | ||
87 | } | ||
88 | |||
89 | if !self.disable.is_empty() { | ||
90 | f.write_str("; ")?; | ||
91 | } | ||
49 | } | 92 | } |
93 | |||
94 | if !self.disable.is_empty() { | ||
95 | f.write_str("disable ")?; | ||
96 | for (i, atom) in self.disable.iter().enumerate() { | ||
97 | let sep = match i { | ||
98 | 0 => "", | ||
99 | _ if i == self.enable.len() - 1 => " and ", | ||
100 | _ => ", ", | ||
101 | }; | ||
102 | f.write_str(sep)?; | ||
103 | |||
104 | write!(f, "{}", atom)?; | ||
105 | } | ||
106 | } | ||
107 | |||
108 | Ok(()) | ||
109 | } | ||
110 | } | ||
111 | |||
112 | pub struct InactiveReason { | ||
113 | enabled: Vec<CfgAtom>, | ||
114 | disabled: Vec<CfgAtom>, | ||
115 | } | ||
116 | |||
117 | impl fmt::Display for InactiveReason { | ||
118 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
119 | if !self.enabled.is_empty() { | ||
120 | for (i, atom) in self.enabled.iter().enumerate() { | ||
121 | let sep = match i { | ||
122 | 0 => "", | ||
123 | _ if i == self.enabled.len() - 1 => " and ", | ||
124 | _ => ", ", | ||
125 | }; | ||
126 | f.write_str(sep)?; | ||
127 | |||
128 | write!(f, "{}", atom)?; | ||
129 | } | ||
130 | let is_are = if self.enabled.len() == 1 { "is" } else { "are" }; | ||
131 | write!(f, " {} enabled", is_are)?; | ||
132 | |||
133 | if !self.disabled.is_empty() { | ||
134 | f.write_str(" and ")?; | ||
135 | } | ||
136 | } | ||
137 | |||
138 | if !self.disabled.is_empty() { | ||
139 | for (i, atom) in self.disabled.iter().enumerate() { | ||
140 | let sep = match i { | ||
141 | 0 => "", | ||
142 | _ if i == self.disabled.len() - 1 => " and ", | ||
143 | _ => ", ", | ||
144 | }; | ||
145 | f.write_str(sep)?; | ||
146 | |||
147 | write!(f, "{}", atom)?; | ||
148 | } | ||
149 | let is_are = if self.disabled.len() == 1 { "is" } else { "are" }; | ||
150 | write!(f, " {} disabled", is_are)?; | ||
151 | } | ||
152 | |||
153 | Ok(()) | ||
50 | } | 154 | } |
51 | } | 155 | } |
diff --git a/crates/cfg/src/tests.rs b/crates/cfg/src/tests.rs new file mode 100644 index 000000000..bd0f9ec48 --- /dev/null +++ b/crates/cfg/src/tests.rs | |||
@@ -0,0 +1,193 @@ | |||
1 | use expect_test::{expect, Expect}; | ||
2 | use mbe::ast_to_token_tree; | ||
3 | use syntax::{ast, AstNode}; | ||
4 | |||
5 | use crate::{CfgAtom, CfgExpr, CfgOptions, DnfExpr}; | ||
6 | |||
7 | fn assert_parse_result(input: &str, expected: CfgExpr) { | ||
8 | let (tt, _) = { | ||
9 | let source_file = ast::SourceFile::parse(input).ok().unwrap(); | ||
10 | let tt = source_file.syntax().descendants().find_map(ast::TokenTree::cast).unwrap(); | ||
11 | ast_to_token_tree(&tt).unwrap() | ||
12 | }; | ||
13 | let cfg = CfgExpr::parse(&tt); | ||
14 | assert_eq!(cfg, expected); | ||
15 | } | ||
16 | |||
17 | fn check_dnf(input: &str, expect: Expect) { | ||
18 | let (tt, _) = { | ||
19 | let source_file = ast::SourceFile::parse(input).ok().unwrap(); | ||
20 | let tt = source_file.syntax().descendants().find_map(ast::TokenTree::cast).unwrap(); | ||
21 | ast_to_token_tree(&tt).unwrap() | ||
22 | }; | ||
23 | let cfg = CfgExpr::parse(&tt); | ||
24 | let actual = format!("#![cfg({})]", DnfExpr::new(cfg)); | ||
25 | expect.assert_eq(&actual); | ||
26 | } | ||
27 | |||
28 | fn check_why_inactive(input: &str, opts: &CfgOptions, expect: Expect) { | ||
29 | let (tt, _) = { | ||
30 | let source_file = ast::SourceFile::parse(input).ok().unwrap(); | ||
31 | let tt = source_file.syntax().descendants().find_map(ast::TokenTree::cast).unwrap(); | ||
32 | ast_to_token_tree(&tt).unwrap() | ||
33 | }; | ||
34 | let cfg = CfgExpr::parse(&tt); | ||
35 | let dnf = DnfExpr::new(cfg); | ||
36 | let why_inactive = dnf.why_inactive(opts).unwrap().to_string(); | ||
37 | expect.assert_eq(&why_inactive); | ||
38 | } | ||
39 | |||
40 | #[track_caller] | ||
41 | fn check_enable_hints(input: &str, opts: &CfgOptions, expected_hints: &[&str]) { | ||
42 | let (tt, _) = { | ||
43 | let source_file = ast::SourceFile::parse(input).ok().unwrap(); | ||
44 | let tt = source_file.syntax().descendants().find_map(ast::TokenTree::cast).unwrap(); | ||
45 | ast_to_token_tree(&tt).unwrap() | ||
46 | }; | ||
47 | let cfg = CfgExpr::parse(&tt); | ||
48 | let dnf = DnfExpr::new(cfg); | ||
49 | let hints = dnf.compute_enable_hints(opts).map(|diff| diff.to_string()).collect::<Vec<_>>(); | ||
50 | assert_eq!(hints, expected_hints); | ||
51 | } | ||
52 | |||
53 | #[test] | ||
54 | fn test_cfg_expr_parser() { | ||
55 | assert_parse_result("#![cfg(foo)]", CfgAtom::Flag("foo".into()).into()); | ||
56 | assert_parse_result("#![cfg(foo,)]", CfgAtom::Flag("foo".into()).into()); | ||
57 | assert_parse_result( | ||
58 | "#![cfg(not(foo))]", | ||
59 | CfgExpr::Not(Box::new(CfgAtom::Flag("foo".into()).into())), | ||
60 | ); | ||
61 | assert_parse_result("#![cfg(foo(bar))]", CfgExpr::Invalid); | ||
62 | |||
63 | // Only take the first | ||
64 | assert_parse_result(r#"#![cfg(foo, bar = "baz")]"#, CfgAtom::Flag("foo".into()).into()); | ||
65 | |||
66 | assert_parse_result( | ||
67 | r#"#![cfg(all(foo, bar = "baz"))]"#, | ||
68 | CfgExpr::All(vec![ | ||
69 | CfgAtom::Flag("foo".into()).into(), | ||
70 | CfgAtom::KeyValue { key: "bar".into(), value: "baz".into() }.into(), | ||
71 | ]), | ||
72 | ); | ||
73 | |||
74 | assert_parse_result( | ||
75 | r#"#![cfg(any(not(), all(), , bar = "baz",))]"#, | ||
76 | CfgExpr::Any(vec![ | ||
77 | CfgExpr::Not(Box::new(CfgExpr::Invalid)), | ||
78 | CfgExpr::All(vec![]), | ||
79 | CfgExpr::Invalid, | ||
80 | CfgAtom::KeyValue { key: "bar".into(), value: "baz".into() }.into(), | ||
81 | ]), | ||
82 | ); | ||
83 | } | ||
84 | |||
85 | #[test] | ||
86 | fn smoke() { | ||
87 | check_dnf("#![cfg(test)]", expect![[r#"#![cfg(test)]"#]]); | ||
88 | check_dnf("#![cfg(not(test))]", expect![[r#"#![cfg(not(test))]"#]]); | ||
89 | check_dnf("#![cfg(not(not(test)))]", expect![[r#"#![cfg(test)]"#]]); | ||
90 | |||
91 | check_dnf("#![cfg(all(a, b))]", expect![[r#"#![cfg(all(a, b))]"#]]); | ||
92 | check_dnf("#![cfg(any(a, b))]", expect![[r#"#![cfg(any(a, b))]"#]]); | ||
93 | |||
94 | check_dnf("#![cfg(not(a))]", expect![[r#"#![cfg(not(a))]"#]]); | ||
95 | } | ||
96 | |||
97 | #[test] | ||
98 | fn distribute() { | ||
99 | check_dnf("#![cfg(all(any(a, b), c))]", expect![[r#"#![cfg(any(all(a, c), all(b, c)))]"#]]); | ||
100 | check_dnf("#![cfg(all(c, any(a, b)))]", expect![[r#"#![cfg(any(all(c, a), all(c, b)))]"#]]); | ||
101 | check_dnf( | ||
102 | "#![cfg(all(any(a, b), any(c, d)))]", | ||
103 | expect![[r#"#![cfg(any(all(a, c), all(a, d), all(b, c), all(b, d)))]"#]], | ||
104 | ); | ||
105 | |||
106 | check_dnf( | ||
107 | "#![cfg(all(any(a, b, c), any(d, e, f), g))]", | ||
108 | expect![[ | ||
109 | r#"#![cfg(any(all(a, d, g), all(a, e, g), all(a, f, g), all(b, d, g), all(b, e, g), all(b, f, g), all(c, d, g), all(c, e, g), all(c, f, g)))]"# | ||
110 | ]], | ||
111 | ); | ||
112 | } | ||
113 | |||
114 | #[test] | ||
115 | fn demorgan() { | ||
116 | check_dnf("#![cfg(not(all(a, b)))]", expect![[r#"#![cfg(any(not(a), not(b)))]"#]]); | ||
117 | check_dnf("#![cfg(not(any(a, b)))]", expect![[r#"#![cfg(all(not(a), not(b)))]"#]]); | ||
118 | |||
119 | check_dnf("#![cfg(not(all(not(a), b)))]", expect![[r#"#![cfg(any(a, not(b)))]"#]]); | ||
120 | check_dnf("#![cfg(not(any(a, not(b))))]", expect![[r#"#![cfg(all(not(a), b))]"#]]); | ||
121 | } | ||
122 | |||
123 | #[test] | ||
124 | fn nested() { | ||
125 | check_dnf("#![cfg(all(any(a), not(all(any(b)))))]", expect![[r#"#![cfg(all(a, not(b)))]"#]]); | ||
126 | |||
127 | check_dnf("#![cfg(any(any(a, b)))]", expect![[r#"#![cfg(any(a, b))]"#]]); | ||
128 | check_dnf("#![cfg(not(any(any(a, b))))]", expect![[r#"#![cfg(all(not(a), not(b)))]"#]]); | ||
129 | check_dnf("#![cfg(all(all(a, b)))]", expect![[r#"#![cfg(all(a, b))]"#]]); | ||
130 | check_dnf("#![cfg(not(all(all(a, b))))]", expect![[r#"#![cfg(any(not(a), not(b)))]"#]]); | ||
131 | } | ||
132 | |||
133 | #[test] | ||
134 | fn hints() { | ||
135 | let mut opts = CfgOptions::default(); | ||
136 | |||
137 | check_enable_hints("#![cfg(test)]", &opts, &["enable test"]); | ||
138 | check_enable_hints("#![cfg(not(test))]", &opts, &[]); | ||
139 | |||
140 | check_enable_hints("#![cfg(any(a, b))]", &opts, &["enable a", "enable b"]); | ||
141 | check_enable_hints("#![cfg(any(b, a))]", &opts, &["enable b", "enable a"]); | ||
142 | |||
143 | check_enable_hints("#![cfg(all(a, b))]", &opts, &["enable a and b"]); | ||
144 | |||
145 | opts.insert_atom("test".into()); | ||
146 | |||
147 | check_enable_hints("#![cfg(test)]", &opts, &[]); | ||
148 | check_enable_hints("#![cfg(not(test))]", &opts, &["disable test"]); | ||
149 | } | ||
150 | |||
151 | /// Tests that we don't suggest hints for cfgs that express an inconsistent formula. | ||
152 | #[test] | ||
153 | fn hints_impossible() { | ||
154 | let mut opts = CfgOptions::default(); | ||
155 | |||
156 | check_enable_hints("#![cfg(all(test, not(test)))]", &opts, &[]); | ||
157 | |||
158 | opts.insert_atom("test".into()); | ||
159 | |||
160 | check_enable_hints("#![cfg(all(test, not(test)))]", &opts, &[]); | ||
161 | } | ||
162 | |||
163 | #[test] | ||
164 | fn why_inactive() { | ||
165 | let mut opts = CfgOptions::default(); | ||
166 | opts.insert_atom("test".into()); | ||
167 | opts.insert_atom("test2".into()); | ||
168 | |||
169 | check_why_inactive("#![cfg(a)]", &opts, expect![["a is disabled"]]); | ||
170 | check_why_inactive("#![cfg(not(test))]", &opts, expect![["test is enabled"]]); | ||
171 | |||
172 | check_why_inactive( | ||
173 | "#![cfg(all(not(test), not(test2)))]", | ||
174 | &opts, | ||
175 | expect![["test and test2 are enabled"]], | ||
176 | ); | ||
177 | check_why_inactive("#![cfg(all(a, b))]", &opts, expect![["a and b are disabled"]]); | ||
178 | check_why_inactive( | ||
179 | "#![cfg(all(not(test), a))]", | ||
180 | &opts, | ||
181 | expect![["test is enabled and a is disabled"]], | ||
182 | ); | ||
183 | check_why_inactive( | ||
184 | "#![cfg(all(not(test), test2, a))]", | ||
185 | &opts, | ||
186 | expect![["test is enabled and a is disabled"]], | ||
187 | ); | ||
188 | check_why_inactive( | ||
189 | "#![cfg(all(not(test), not(test2), a))]", | ||
190 | &opts, | ||
191 | expect![["test and test2 are enabled and a is disabled"]], | ||
192 | ); | ||
193 | } | ||