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 library status_expression; | 5 /// A parsed Boolean expression AST. |
6 | 6 abstract class Expression { |
kevmoo
2017/05/15 21:06:48
Possible? https://github.com/dart-lang/boolean_sel
Bob Nystrom
2017/05/15 22:42:51
For test.dart, we try not to rely on any external
| |
7 /** | 7 /// Parses Boolean expressions in a .status file for Dart. |
8 * Parse and evaluate expressions in a .status file for Dart and V8. | 8 /// |
9 * There are set expressions and Boolean expressions in a .status file. | 9 /// The grammar is: |
10 * The grammar is: | 10 /// |
11 * BooleanExpression := $variableName == value | $variableName != value | | 11 /// expression := or |
12 * $variableName | (BooleanExpression) | | 12 /// or := and ( "||" and )* |
13 * BooleanExpression && BooleanExpression | | 13 /// and := primary ( "&&" primary )* |
14 * BooleanExpression || BooleanExpression | 14 /// primary := "$" identifier ( ( "==" | "!=" ) identifier )? | |
15 * | 15 /// "(" expression ")" |
16 * SetExpression := value | (SetExpression) | | 16 /// identifier := regex "\w+" |
17 * SetExpression || SetExpression | | 17 /// |
18 * SetExpression if BooleanExpression | | 18 /// Expressions evaluate as expected, with values of variables found in an |
19 * SetExpression , SetExpression | 19 /// environment passed to the evaluator. |
20 * | 20 static Expression parse(String expression) => |
21 * Productions are listed in order of precedence, and the || and , operators | 21 new _ExpressionParser(expression).parse(); |
22 * both evaluate to set union, but with different precedence. | 22 |
23 * | 23 /// Evaluates the expression where all variables are defined by the given |
24 * Values and variableNames are non-empty strings of word characters, matching | 24 /// [environment]. |
25 * the RegExp \w+. | 25 bool evaluate(Map<String, dynamic> environment); |
26 * | 26 } |
27 * Expressions evaluate as expected, with values of variables found in | 27 |
28 * an environment passed to the evaluator. The SetExpression "value" | 28 /// Keyword token strings. |
29 * evaluates to a singleton set containing that value. "A if B" evaluates | 29 class _Token { |
30 * to A if B is true, and to the empty set if B is false. | 30 static const leftParen = "("; |
31 */ | 31 static const rightParen = ")"; |
32 | 32 static const dollar = r"$"; |
33 class ExprEvaluationException { | 33 static const equals = "=="; |
34 String error; | 34 static const notEqual = "!="; |
35 | 35 static const and = "&&"; |
36 ExprEvaluationException(this.error); | 36 static const or = "||"; |
37 | 37 } |
38 toString() => error; | 38 |
39 } | 39 /// A reference to a variable. |
40 | 40 class _Variable { |
41 class Token { | 41 final String name; |
42 static const String LEFT_PAREN = "("; | 42 |
43 static const String RIGHT_PAREN = ")"; | 43 _Variable(this.name); |
44 static const String DOLLAR_SYMBOL = r"$"; | 44 |
45 static const String UNION = ","; | 45 dynamic lookup(Map<String, dynamic> environment) { |
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) { | |
102 var value = environment[name]; | 46 var value = environment[name]; |
103 if (value == null) { | 47 if (value == null) { |
104 throw new ExprEvaluationException("Could not find '$name' in environment " | 48 throw new Exception("Could not find '$name' in environment " |
105 "while evaluating status file expression."); | 49 "while evaluating status file expression."); |
106 } | 50 } |
107 return value.toString(); | 51 |
108 } | 52 return value; |
109 } | 53 } |
110 | 54 } |
111 class TermConstant { | 55 |
112 String value; | 56 /// Tests whether a given variable is or is not equal some literal value, as in: |
113 | 57 /// |
114 TermConstant(String this.value); | 58 /// $variable == someValue |
115 | 59 class _ComparisonExpression implements Expression { |
116 String termValue(environment) => value; | 60 final _Variable left; |
117 } | 61 final String right; |
118 | 62 final bool negate; |
119 class BooleanVariable implements BooleanExpression { | 63 |
120 TermVariable variable; | 64 _ComparisonExpression(this.left, this.right, this.negate); |
121 | 65 |
122 BooleanVariable(this.variable); | 66 bool evaluate(Map<String, dynamic> environment) { |
123 | 67 return negate != (left.lookup(environment) == right); |
124 bool evaluate(environment) => variable.termValue(environment) == 'true'; | 68 } |
69 | |
70 String toString() => "(\$${left.name} ${negate ? '!=' : '=='} $right)"; | |
71 } | |
72 | |
73 /// A reference to a variable defined in the environment. The variable's value | |
74 /// should be Boolean and the expression is true when the value is "true". | |
75 /// | |
76 /// $variable | |
77 class _VariableExpression implements Expression { | |
78 final _Variable variable; | |
79 | |
80 _VariableExpression(this.variable); | |
81 | |
82 bool evaluate(Map<String, dynamic> environment) => | |
83 variable.lookup(environment) == 'true'; | |
84 | |
125 String toString() => "(bool \$${variable.name})"; | 85 String toString() => "(bool \$${variable.name})"; |
126 } | 86 } |
127 | 87 |
128 class BooleanOperation implements BooleanExpression { | 88 /// A logical `||` or `&&` expression. |
129 String op; | 89 class _LogicExpression implements Expression { |
130 BooleanExpression left; | 90 /// The operator, `||` or `&&`. |
131 BooleanExpression right; | 91 final String op; |
132 | 92 |
133 BooleanOperation(this.op, this.left, this.right); | 93 final Expression left; |
134 | 94 final Expression right; |
135 bool evaluate(environment) => (op == Token.AND) | 95 |
96 _LogicExpression(this.op, this.left, this.right); | |
97 | |
98 bool evaluate(Map<String, dynamic> environment) => (op == _Token.and) | |
136 ? left.evaluate(environment) && right.evaluate(environment) | 99 ? left.evaluate(environment) && right.evaluate(environment) |
137 : left.evaluate(environment) || right.evaluate(environment); | 100 : left.evaluate(environment) || right.evaluate(environment); |
101 | |
138 String toString() => "($left $op $right)"; | 102 String toString() => "($left $op $right)"; |
139 } | 103 } |
140 | 104 |
141 class SetUnion implements SetExpression { | 105 /// Parser for Boolean expressions in a .status file for Dart. |
142 SetExpression left; | 106 class _ExpressionParser { |
143 SetExpression right; | 107 final _Scanner _scanner; |
144 | 108 |
145 SetUnion(this.left, this.right); | 109 _ExpressionParser(String expression) : _scanner = new _Scanner(expression); |
146 | 110 |
147 // Overwrites left.evaluate(env). | 111 Expression parse() { |
148 // Set.addAll does not return this. | 112 var expression = _parseOr(); |
149 Set<String> evaluate(environment) { | 113 |
150 Set<String> result = left.evaluate(environment); | 114 // Should consume entire string. |
151 result.addAll(right.evaluate(environment)); | 115 if (_scanner.hasMore) { |
152 return result; | 116 throw new FormatException("Unexpected input after expression"); |
153 } | 117 } |
154 | 118 |
155 String toString() => "($left || $right)"; | 119 return expression; |
156 } | 120 } |
157 | 121 |
158 class SetIf implements SetExpression { | 122 Expression _parseOr() { |
159 SetExpression left; | 123 var left = _parseAnd(); |
160 BooleanExpression right; | 124 while (_scanner.match(_Token.or)) { |
161 | 125 var right = _parseAnd(); |
162 SetIf(this.left, this.right); | 126 left = new _LogicExpression(_Token.or, left, right); |
163 | 127 } |
164 Set<String> evaluate(environment) => right.evaluate(environment) | 128 |
165 ? left.evaluate(environment) | |
166 : new Set<String>(); | |
167 String toString() => "($left if $right)"; | |
168 } | |
169 | |
170 class SetConstant implements SetExpression { | |
171 String value; | |
172 | |
173 SetConstant(String v) : value = v.toLowerCase(); | |
174 | |
175 Set<String> evaluate(environment) => [value].toSet(); | |
176 String toString() => value; | |
177 } | |
178 | |
179 // An iterator that allows peeking at the current token. | |
180 class Scanner { | |
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; | 129 return left; |
212 } | 130 } |
213 | 131 |
214 SetExpression parseSetIf() { | 132 Expression _parseAnd() { |
215 SetExpression left = parseSetOr(); | 133 var left = _parsePrimary(); |
216 while (scanner.hasMore() && scanner.current == "if") { | 134 while (_scanner.match(_Token.and)) { |
217 scanner.advance(); | 135 var right = _parsePrimary(); |
218 BooleanExpression right = parseBooleanExpression(); | 136 left = new _LogicExpression(_Token.and, left, right); |
219 left = new SetIf(left, right); | 137 } |
220 } | 138 |
221 return left; | 139 return left; |
222 } | 140 } |
223 | 141 |
224 SetExpression parseSetOr() { | 142 Expression _parsePrimary() { |
225 SetExpression left = parseSetAtomic(); | 143 if (_scanner.match(_Token.leftParen)) { |
226 while (scanner.hasMore() && scanner.current == Token.OR) { | 144 var value = _parseOr(); |
227 scanner.advance(); | 145 if (!_scanner.match(_Token.rightParen)) { |
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) { | |
239 throw new FormatException("Missing right parenthesis in expression"); | 146 throw new FormatException("Missing right parenthesis in expression"); |
240 } | 147 } |
241 scanner.advance(); | 148 |
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(); | |
283 return value; | 149 return value; |
284 } | 150 } |
285 | 151 |
286 // The only atomic booleans are of the form $variable == value or | 152 // The only atomic booleans are of the form $variable == value or |
287 // of the form $variable. | 153 // of the form $variable. |
288 if (scanner.current != Token.DOLLAR_SYMBOL) { | 154 if (!_scanner.match(_Token.dollar)) { |
289 throw new FormatException( | 155 throw new FormatException( |
290 "Expected \$ in expression, got ${scanner.current}"); | 156 "Expected \$ in expression, got ${_scanner.current}"); |
291 } | 157 } |
292 scanner.advance(); | 158 |
293 if (!new RegExp(r"^\w+$").hasMatch(scanner.current)) { | 159 if (!_scanner.isIdentifier) { |
294 throw new FormatException( | 160 throw new FormatException( |
295 "Expected identifier in expression, got ${scanner.current}"); | 161 "Expected identifier in expression, got ${_scanner.current}"); |
296 } | 162 } |
297 TermVariable left = new TermVariable(scanner.current); | 163 |
298 scanner.advance(); | 164 var left = new _Variable(_scanner.current); |
299 if (scanner.current == Token.EQUALS || | 165 _scanner.advance(); |
300 scanner.current == Token.NOT_EQUALS) { | 166 |
301 bool negate = scanner.current == Token.NOT_EQUALS; | 167 if (_scanner.current == _Token.equals || |
302 scanner.advance(); | 168 _scanner.current == _Token.notEqual) { |
303 if (!new RegExp(r"^\w+$").hasMatch(scanner.current)) { | 169 var negate = _scanner.advance() == _Token.notEqual; |
170 | |
171 if (!_scanner.isIdentifier) { | |
304 throw new FormatException( | 172 throw new FormatException( |
305 "Expected value in expression, got ${scanner.current}"); | 173 "Expected value in expression, got ${_scanner.current}"); |
306 } | 174 } |
307 TermConstant right = new TermConstant(scanner.current); | 175 |
308 scanner.advance(); | 176 var right = _scanner.advance(); |
309 return new Comparison(left, right, negate); | 177 return new _ComparisonExpression(left, right, negate); |
310 } else { | 178 } else { |
311 return new BooleanVariable(left); | 179 return new _VariableExpression(left); |
312 } | 180 } |
313 } | 181 } |
314 } | 182 } |
183 | |
184 /// An iterator that allows peeking at the current token. | |
185 class _Scanner { | |
186 /// Tokens are "(", ")", "$", ",", "&&", "||", "==", "!=", and (maximal) \w+. | |
187 static final _testPattern = | |
188 new RegExp(r"^([()$\w\s,]|(\&\&)|(\|\|)|(\=\=)|(\!\=))+$"); | |
Bill Hesse
2017/05/15 16:15:08
I think you can remove ',' from the two regexps, i
Bob Nystrom
2017/05/15 22:42:51
Nice catch! Done.
| |
189 static final _tokenPattern = | |
190 new RegExp(r"[()$,]|(\&\&)|(\|\|)|(\=\=)|(\!\=)|\w+"); | |
191 | |
192 static final _identifierPattern = new RegExp(r"^\w+$"); | |
193 | |
194 /// The token strings being iterated. | |
195 final Iterator<String> tokenIterator; | |
196 | |
197 String current; | |
198 | |
199 _Scanner(String expression) : tokenIterator = tokenize(expression).iterator { | |
200 advance(); | |
201 } | |
202 | |
203 static List<String> tokenize(String expression) { | |
204 if (!_testPattern.hasMatch(expression)) { | |
205 throw new FormatException("Syntax error in '$expression'"); | |
206 } | |
207 | |
208 return _tokenPattern | |
209 .allMatches(expression) | |
Bill Hesse
2017/05/15 16:15:09
I had no idea that this was non-overlapping. You
Bob Nystrom
2017/05/15 22:42:51
Guess so. I did too! (This was in the original cod
| |
210 .map((match) => match[0]) | |
211 .toList(); | |
212 } | |
213 | |
214 bool get hasMore => current != null; | |
215 | |
216 /// Returns `true` if the current token is an identifier. | |
217 bool get isIdentifier => _identifierPattern.hasMatch(current); | |
218 | |
219 /// If the current token is [token], consumes it and returns `true`. | |
220 bool match(String token) { | |
221 if (!hasMore || current != token) return false; | |
222 | |
223 advance(); | |
224 return true; | |
225 } | |
226 | |
227 /// Consumes the current token and returns it. | |
228 String advance() { | |
229 var previous = current; | |
230 current = tokenIterator.moveNext() ? tokenIterator.current : null; | |
231 return previous; | |
232 } | |
233 } | |
OLD | NEW |