OLD | NEW |
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 /// A parsed Boolean expression AST. | 5 library status_expression; |
6 abstract class Expression { | 6 |
7 /// Parses Boolean expressions in a .status file for Dart. | 7 /** |
8 /// | 8 * Parse and evaluate expressions in a .status file for Dart and V8. |
9 /// The grammar is: | 9 * There are set expressions and Boolean expressions in a .status file. |
10 /// | 10 * The grammar is: |
11 /// expression := or | 11 * BooleanExpression := $variableName == value | $variableName != value | |
12 /// or := and ( "||" and )* | 12 * $variableName | (BooleanExpression) | |
13 /// and := primary ( "&&" primary )* | 13 * BooleanExpression && BooleanExpression | |
14 /// primary := "$" identifier ( ( "==" | "!=" ) identifier )? | | 14 * BooleanExpression || BooleanExpression |
15 /// "(" expression ")" | 15 * |
16 /// identifier := regex "\w+" | 16 * SetExpression := value | (SetExpression) | |
17 /// | 17 * SetExpression || SetExpression | |
18 /// Expressions evaluate as expected, with values of variables found in an | 18 * SetExpression if BooleanExpression | |
19 /// environment passed to the evaluator. | 19 * SetExpression , SetExpression |
20 static Expression parse(String expression) => | 20 * |
21 new _ExpressionParser(expression).parse(); | 21 * Productions are listed in order of precedence, and the || and , operators |
22 | 22 * both evaluate to set union, but with different precedence. |
23 /// Evaluates the expression where all variables are defined by the given | 23 * |
24 /// [environment]. | 24 * Values and variableNames are non-empty strings of word characters, matching |
25 bool evaluate(Map<String, dynamic> environment); | 25 * the RegExp \w+. |
26 } | 26 * |
27 | 27 * Expressions evaluate as expected, with values of variables found in |
28 /// Keyword token strings. | 28 * an environment passed to the evaluator. The SetExpression "value" |
29 class _Token { | 29 * evaluates to a singleton set containing that value. "A if B" evaluates |
30 static const leftParen = "("; | 30 * to A if B is true, and to the empty set if B is false. |
31 static const rightParen = ")"; | 31 */ |
32 static const dollar = r"$"; | 32 |
33 static const equals = "=="; | 33 class ExprEvaluationException { |
34 static const notEqual = "!="; | 34 String error; |
35 static const and = "&&"; | 35 |
36 static const or = "||"; | 36 ExprEvaluationException(this.error); |
37 } | 37 |
38 | 38 toString() => error; |
39 /// A reference to a variable. | 39 } |
40 class _Variable { | 40 |
41 final String name; | 41 class Token { |
42 | 42 static const String LEFT_PAREN = "("; |
43 _Variable(this.name); | 43 static const String RIGHT_PAREN = ")"; |
44 | 44 static const String DOLLAR_SYMBOL = r"$"; |
45 String lookup(Map<String, dynamic> environment) { | 45 static const String UNION = ","; |
| 46 static const String EQUALS = "=="; |
| 47 static const String NOT_EQUALS = "!="; |
| 48 static const String AND = "&&"; |
| 49 static const String OR = "||"; |
| 50 } |
| 51 |
| 52 class Tokenizer { |
| 53 String expression; |
| 54 List<String> tokens; |
| 55 |
| 56 Tokenizer(String this.expression) : tokens = new List<String>(); |
| 57 |
| 58 // Tokens are : "(", ")", "$", ",", "&&", "||", "==", "!=", and (maximal) \w+. |
| 59 static final testRegexp = |
| 60 new RegExp(r"^([()$\w\s,]|(\&\&)|(\|\|)|(\=\=)|(\!\=))+$"); |
| 61 static final regexp = new RegExp(r"[()$,]|(\&\&)|(\|\|)|(\=\=)|(\!\=)|\w+"); |
| 62 |
| 63 List<String> tokenize() { |
| 64 if (!testRegexp.hasMatch(expression)) { |
| 65 throw new FormatException("Syntax error in '$expression'"); |
| 66 } |
| 67 for (Match match in regexp.allMatches(expression)) tokens.add(match[0]); |
| 68 return tokens; |
| 69 } |
| 70 } |
| 71 |
| 72 abstract class BooleanExpression { |
| 73 bool evaluate(Map<String, String> environment); |
| 74 } |
| 75 |
| 76 abstract class SetExpression { |
| 77 Set<String> evaluate(Map<String, String> environment); |
| 78 } |
| 79 |
| 80 class Comparison implements BooleanExpression { |
| 81 TermVariable left; |
| 82 TermConstant right; |
| 83 bool negate; |
| 84 |
| 85 Comparison(this.left, this.right, this.negate); |
| 86 |
| 87 bool evaluate(environment) { |
| 88 return negate != |
| 89 (left.termValue(environment) == right.termValue(environment)); |
| 90 } |
| 91 |
| 92 String toString() => |
| 93 "(\$${left.name} ${negate ? '!=' : '=='} ${right.value})"; |
| 94 } |
| 95 |
| 96 class TermVariable { |
| 97 String name; |
| 98 |
| 99 TermVariable(this.name); |
| 100 |
| 101 String termValue(environment) { |
46 var value = environment[name]; | 102 var value = environment[name]; |
47 if (value == null) { | 103 if (value == null) { |
48 throw new Exception("Could not find '$name' in environment " | 104 throw new ExprEvaluationException("Could not find '$name' in environment " |
49 "while evaluating status file expression."); | 105 "while evaluating status file expression."); |
50 } | 106 } |
51 | |
52 // Explicitly stringify all values so that things like: | |
53 // | |
54 // $strong == true | |
55 // | |
56 // work correctly even though "true" is treated as a string here. | |
57 // TODO(rnystrom): Is there a cleaner/safer way to do this? | |
58 return value.toString(); | 107 return value.toString(); |
59 } | 108 } |
60 } | 109 } |
61 | 110 |
62 /// Tests whether a given variable is or is not equal some literal value, as in: | 111 class TermConstant { |
63 /// | 112 String value; |
64 /// $variable == someValue | 113 |
65 class _ComparisonExpression implements Expression { | 114 TermConstant(String this.value); |
66 final _Variable left; | 115 |
67 final String right; | 116 String termValue(environment) => value; |
68 final bool negate; | 117 } |
69 | 118 |
70 _ComparisonExpression(this.left, this.right, this.negate); | 119 class BooleanVariable implements BooleanExpression { |
71 | 120 TermVariable variable; |
72 bool evaluate(Map<String, dynamic> environment) { | 121 |
73 return negate != (left.lookup(environment) == right); | 122 BooleanVariable(this.variable); |
74 } | 123 |
75 | 124 bool evaluate(environment) => variable.termValue(environment) == 'true'; |
76 String toString() => "(\$${left.name} ${negate ? '!=' : '=='} $right)"; | |
77 } | |
78 | |
79 /// A reference to a variable defined in the environment. The expression | |
80 /// evaluates to true if the variable's stringified value is "true". | |
81 /// | |
82 /// $variable | |
83 class _VariableExpression implements Expression { | |
84 final _Variable variable; | |
85 | |
86 _VariableExpression(this.variable); | |
87 | |
88 bool evaluate(Map<String, dynamic> environment) => | |
89 variable.lookup(environment) == "true"; | |
90 | |
91 String toString() => "(bool \$${variable.name})"; | 125 String toString() => "(bool \$${variable.name})"; |
92 } | 126 } |
93 | 127 |
94 /// A logical `||` or `&&` expression. | 128 class BooleanOperation implements BooleanExpression { |
95 class _LogicExpression implements Expression { | 129 String op; |
96 /// The operator, `||` or `&&`. | 130 BooleanExpression left; |
97 final String op; | 131 BooleanExpression right; |
98 | 132 |
99 final Expression left; | 133 BooleanOperation(this.op, this.left, this.right); |
100 final Expression right; | 134 |
101 | 135 bool evaluate(environment) => (op == Token.AND) |
102 _LogicExpression(this.op, this.left, this.right); | |
103 | |
104 bool evaluate(Map<String, dynamic> environment) => (op == _Token.and) | |
105 ? left.evaluate(environment) && right.evaluate(environment) | 136 ? left.evaluate(environment) && right.evaluate(environment) |
106 : left.evaluate(environment) || right.evaluate(environment); | 137 : left.evaluate(environment) || right.evaluate(environment); |
107 | |
108 String toString() => "($left $op $right)"; | 138 String toString() => "($left $op $right)"; |
109 } | 139 } |
110 | 140 |
111 /// Parser for Boolean expressions in a .status file for Dart. | 141 class SetUnion implements SetExpression { |
112 class _ExpressionParser { | 142 SetExpression left; |
113 final _Scanner _scanner; | 143 SetExpression right; |
114 | 144 |
115 _ExpressionParser(String expression) : _scanner = new _Scanner(expression); | 145 SetUnion(this.left, this.right); |
116 | 146 |
117 Expression parse() { | 147 // Overwrites left.evaluate(env). |
118 var expression = _parseOr(); | 148 // Set.addAll does not return this. |
119 | 149 Set<String> evaluate(environment) { |
120 // Should consume entire string. | 150 Set<String> result = left.evaluate(environment); |
121 if (_scanner.hasMore) { | 151 result.addAll(right.evaluate(environment)); |
122 throw new FormatException("Unexpected input after expression"); | 152 return result; |
123 } | 153 } |
124 | 154 |
125 return expression; | 155 String toString() => "($left || $right)"; |
126 } | 156 } |
127 | 157 |
128 Expression _parseOr() { | 158 class SetIf implements SetExpression { |
129 var left = _parseAnd(); | 159 SetExpression left; |
130 while (_scanner.match(_Token.or)) { | 160 BooleanExpression right; |
131 var right = _parseAnd(); | 161 |
132 left = new _LogicExpression(_Token.or, left, right); | 162 SetIf(this.left, this.right); |
133 } | 163 |
134 | 164 Set<String> evaluate(environment) => right.evaluate(environment) |
135 return left; | 165 ? left.evaluate(environment) |
136 } | 166 : new Set<String>(); |
137 | 167 String toString() => "($left if $right)"; |
138 Expression _parseAnd() { | 168 } |
139 var left = _parsePrimary(); | 169 |
140 while (_scanner.match(_Token.and)) { | 170 class SetConstant implements SetExpression { |
141 var right = _parsePrimary(); | 171 String value; |
142 left = new _LogicExpression(_Token.and, left, right); | 172 |
143 } | 173 SetConstant(String v) : value = v.toLowerCase(); |
144 | 174 |
145 return left; | 175 Set<String> evaluate(environment) => [value].toSet(); |
146 } | 176 String toString() => value; |
147 | 177 } |
148 Expression _parsePrimary() { | 178 |
149 if (_scanner.match(_Token.leftParen)) { | 179 // An iterator that allows peeking at the current token. |
150 var value = _parseOr(); | 180 class Scanner { |
151 if (!_scanner.match(_Token.rightParen)) { | 181 List<String> tokens; |
| 182 Iterator tokenIterator; |
| 183 String current; |
| 184 |
| 185 Scanner(this.tokens) { |
| 186 tokenIterator = tokens.iterator; |
| 187 advance(); |
| 188 } |
| 189 |
| 190 bool hasMore() => current != null; |
| 191 |
| 192 void advance() { |
| 193 current = tokenIterator.moveNext() ? tokenIterator.current : null; |
| 194 } |
| 195 } |
| 196 |
| 197 class ExpressionParser { |
| 198 Scanner scanner; |
| 199 |
| 200 ExpressionParser(this.scanner); |
| 201 |
| 202 SetExpression parseSetExpression() => parseSetUnion(); |
| 203 |
| 204 SetExpression parseSetUnion() { |
| 205 SetExpression left = parseSetIf(); |
| 206 while (scanner.hasMore() && scanner.current == Token.UNION) { |
| 207 scanner.advance(); |
| 208 SetExpression right = parseSetIf(); |
| 209 left = new SetUnion(left, right); |
| 210 } |
| 211 return left; |
| 212 } |
| 213 |
| 214 SetExpression parseSetIf() { |
| 215 SetExpression left = parseSetOr(); |
| 216 while (scanner.hasMore() && scanner.current == "if") { |
| 217 scanner.advance(); |
| 218 BooleanExpression right = parseBooleanExpression(); |
| 219 left = new SetIf(left, right); |
| 220 } |
| 221 return left; |
| 222 } |
| 223 |
| 224 SetExpression parseSetOr() { |
| 225 SetExpression left = parseSetAtomic(); |
| 226 while (scanner.hasMore() && scanner.current == Token.OR) { |
| 227 scanner.advance(); |
| 228 SetExpression right = parseSetAtomic(); |
| 229 left = new SetUnion(left, right); |
| 230 } |
| 231 return left; |
| 232 } |
| 233 |
| 234 SetExpression parseSetAtomic() { |
| 235 if (scanner.current == Token.LEFT_PAREN) { |
| 236 scanner.advance(); |
| 237 SetExpression value = parseSetExpression(); |
| 238 if (scanner.current != Token.RIGHT_PAREN) { |
152 throw new FormatException("Missing right parenthesis in expression"); | 239 throw new FormatException("Missing right parenthesis in expression"); |
153 } | 240 } |
154 | 241 scanner.advance(); |
| 242 return value; |
| 243 } |
| 244 if (!new RegExp(r"^\w+$").hasMatch(scanner.current)) { |
| 245 throw new FormatException( |
| 246 "Expected identifier in expression, got ${scanner.current}"); |
| 247 } |
| 248 SetExpression value = new SetConstant(scanner.current); |
| 249 scanner.advance(); |
| 250 return value; |
| 251 } |
| 252 |
| 253 BooleanExpression parseBooleanExpression() => parseBooleanOr(); |
| 254 |
| 255 BooleanExpression parseBooleanOr() { |
| 256 BooleanExpression left = parseBooleanAnd(); |
| 257 while (scanner.hasMore() && scanner.current == Token.OR) { |
| 258 scanner.advance(); |
| 259 BooleanExpression right = parseBooleanAnd(); |
| 260 left = new BooleanOperation(Token.OR, left, right); |
| 261 } |
| 262 return left; |
| 263 } |
| 264 |
| 265 BooleanExpression parseBooleanAnd() { |
| 266 BooleanExpression left = parseBooleanAtomic(); |
| 267 while (scanner.hasMore() && scanner.current == Token.AND) { |
| 268 scanner.advance(); |
| 269 BooleanExpression right = parseBooleanAtomic(); |
| 270 left = new BooleanOperation(Token.AND, left, right); |
| 271 } |
| 272 return left; |
| 273 } |
| 274 |
| 275 BooleanExpression parseBooleanAtomic() { |
| 276 if (scanner.current == Token.LEFT_PAREN) { |
| 277 scanner.advance(); |
| 278 BooleanExpression value = parseBooleanExpression(); |
| 279 if (scanner.current != Token.RIGHT_PAREN) { |
| 280 throw new FormatException("Missing right parenthesis in expression"); |
| 281 } |
| 282 scanner.advance(); |
155 return value; | 283 return value; |
156 } | 284 } |
157 | 285 |
158 // The only atomic booleans are of the form $variable == value or | 286 // The only atomic booleans are of the form $variable == value or |
159 // of the form $variable. | 287 // of the form $variable. |
160 if (!_scanner.match(_Token.dollar)) { | 288 if (scanner.current != Token.DOLLAR_SYMBOL) { |
161 throw new FormatException( | 289 throw new FormatException( |
162 "Expected \$ in expression, got ${_scanner.current}"); | 290 "Expected \$ in expression, got ${scanner.current}"); |
163 } | 291 } |
164 | 292 scanner.advance(); |
165 if (!_scanner.isIdentifier) { | 293 if (!new RegExp(r"^\w+$").hasMatch(scanner.current)) { |
166 throw new FormatException( | 294 throw new FormatException( |
167 "Expected identifier in expression, got ${_scanner.current}"); | 295 "Expected identifier in expression, got ${scanner.current}"); |
168 } | 296 } |
169 | 297 TermVariable left = new TermVariable(scanner.current); |
170 var left = new _Variable(_scanner.current); | 298 scanner.advance(); |
171 _scanner.advance(); | 299 if (scanner.current == Token.EQUALS || |
172 | 300 scanner.current == Token.NOT_EQUALS) { |
173 if (_scanner.current == _Token.equals || | 301 bool negate = scanner.current == Token.NOT_EQUALS; |
174 _scanner.current == _Token.notEqual) { | 302 scanner.advance(); |
175 var negate = _scanner.advance() == _Token.notEqual; | 303 if (!new RegExp(r"^\w+$").hasMatch(scanner.current)) { |
176 | |
177 if (!_scanner.isIdentifier) { | |
178 throw new FormatException( | 304 throw new FormatException( |
179 "Expected value in expression, got ${_scanner.current}"); | 305 "Expected value in expression, got ${scanner.current}"); |
180 } | 306 } |
181 | 307 TermConstant right = new TermConstant(scanner.current); |
182 var right = _scanner.advance(); | 308 scanner.advance(); |
183 return new _ComparisonExpression(left, right, negate); | 309 return new Comparison(left, right, negate); |
184 } else { | 310 } else { |
185 return new _VariableExpression(left); | 311 return new BooleanVariable(left); |
186 } | 312 } |
187 } | 313 } |
188 } | 314 } |
189 | |
190 /// An iterator that allows peeking at the current token. | |
191 class _Scanner { | |
192 /// Tokens are "(", ")", "$", "&&", "||", "==", "!=", and (maximal) \w+. | |
193 static final _testPattern = | |
194 new RegExp(r"^([()$\w\s]|(\&\&)|(\|\|)|(\=\=)|(\!\=))+$"); | |
195 static final _tokenPattern = | |
196 new RegExp(r"[()$]|(\&\&)|(\|\|)|(\=\=)|(\!\=)|\w+"); | |
197 | |
198 static final _identifierPattern = new RegExp(r"^\w+$"); | |
199 | |
200 /// The token strings being iterated. | |
201 final Iterator<String> tokenIterator; | |
202 | |
203 String current; | |
204 | |
205 _Scanner(String expression) : tokenIterator = tokenize(expression).iterator { | |
206 advance(); | |
207 } | |
208 | |
209 static List<String> tokenize(String expression) { | |
210 if (!_testPattern.hasMatch(expression)) { | |
211 throw new FormatException("Syntax error in '$expression'"); | |
212 } | |
213 | |
214 return _tokenPattern | |
215 .allMatches(expression) | |
216 .map((match) => match[0]) | |
217 .toList(); | |
218 } | |
219 | |
220 bool get hasMore => current != null; | |
221 | |
222 /// Returns `true` if the current token is an identifier. | |
223 bool get isIdentifier => _identifierPattern.hasMatch(current); | |
224 | |
225 /// If the current token is [token], consumes it and returns `true`. | |
226 bool match(String token) { | |
227 if (!hasMore || current != token) return false; | |
228 | |
229 advance(); | |
230 return true; | |
231 } | |
232 | |
233 /// Consumes the current token and returns it. | |
234 String advance() { | |
235 var previous = current; | |
236 current = tokenIterator.moveNext() ? tokenIterator.current : null; | |
237 return previous; | |
238 } | |
239 } | |
OLD | NEW |