| 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 { |
| 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 String 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 } |
| 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? |
| 107 return value.toString(); | 58 return value.toString(); |
| 108 } | 59 } |
| 109 } | 60 } |
| 110 | 61 |
| 111 class TermConstant { | 62 /// Tests whether a given variable is or is not equal some literal value, as in: |
| 112 String value; | 63 /// |
| 113 | 64 /// $variable == someValue |
| 114 TermConstant(String this.value); | 65 class _ComparisonExpression implements Expression { |
| 115 | 66 final _Variable left; |
| 116 String termValue(environment) => value; | 67 final String right; |
| 117 } | 68 final bool negate; |
| 118 | 69 |
| 119 class BooleanVariable implements BooleanExpression { | 70 _ComparisonExpression(this.left, this.right, this.negate); |
| 120 TermVariable variable; | 71 |
| 121 | 72 bool evaluate(Map<String, dynamic> environment) { |
| 122 BooleanVariable(this.variable); | 73 return negate != (left.lookup(environment) == right); |
| 123 | 74 } |
| 124 bool evaluate(environment) => variable.termValue(environment) == 'true'; | 75 |
| 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 |
| 125 String toString() => "(bool \$${variable.name})"; | 91 String toString() => "(bool \$${variable.name})"; |
| 126 } | 92 } |
| 127 | 93 |
| 128 class BooleanOperation implements BooleanExpression { | 94 /// A logical `||` or `&&` expression. |
| 129 String op; | 95 class _LogicExpression implements Expression { |
| 130 BooleanExpression left; | 96 /// The operator, `||` or `&&`. |
| 131 BooleanExpression right; | 97 final String op; |
| 132 | 98 |
| 133 BooleanOperation(this.op, this.left, this.right); | 99 final Expression left; |
| 134 | 100 final Expression right; |
| 135 bool evaluate(environment) => (op == Token.AND) | 101 |
| 102 _LogicExpression(this.op, this.left, this.right); |
| 103 |
| 104 bool evaluate(Map<String, dynamic> environment) => (op == _Token.and) |
| 136 ? left.evaluate(environment) && right.evaluate(environment) | 105 ? left.evaluate(environment) && right.evaluate(environment) |
| 137 : left.evaluate(environment) || right.evaluate(environment); | 106 : left.evaluate(environment) || right.evaluate(environment); |
| 107 |
| 138 String toString() => "($left $op $right)"; | 108 String toString() => "($left $op $right)"; |
| 139 } | 109 } |
| 140 | 110 |
| 141 class SetUnion implements SetExpression { | 111 /// Parser for Boolean expressions in a .status file for Dart. |
| 142 SetExpression left; | 112 class _ExpressionParser { |
| 143 SetExpression right; | 113 final _Scanner _scanner; |
| 144 | 114 |
| 145 SetUnion(this.left, this.right); | 115 _ExpressionParser(String expression) : _scanner = new _Scanner(expression); |
| 146 | 116 |
| 147 // Overwrites left.evaluate(env). | 117 Expression parse() { |
| 148 // Set.addAll does not return this. | 118 var expression = _parseOr(); |
| 149 Set<String> evaluate(environment) { | 119 |
| 150 Set<String> result = left.evaluate(environment); | 120 // Should consume entire string. |
| 151 result.addAll(right.evaluate(environment)); | 121 if (_scanner.hasMore) { |
| 152 return result; | 122 throw new FormatException("Unexpected input after expression"); |
| 153 } | 123 } |
| 154 | 124 |
| 155 String toString() => "($left || $right)"; | 125 return expression; |
| 156 } | 126 } |
| 157 | 127 |
| 158 class SetIf implements SetExpression { | 128 Expression _parseOr() { |
| 159 SetExpression left; | 129 var left = _parseAnd(); |
| 160 BooleanExpression right; | 130 while (_scanner.match(_Token.or)) { |
| 161 | 131 var right = _parseAnd(); |
| 162 SetIf(this.left, this.right); | 132 left = new _LogicExpression(_Token.or, left, right); |
| 163 | 133 } |
| 164 Set<String> evaluate(environment) => right.evaluate(environment) | 134 |
| 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; | 135 return left; |
| 212 } | 136 } |
| 213 | 137 |
| 214 SetExpression parseSetIf() { | 138 Expression _parseAnd() { |
| 215 SetExpression left = parseSetOr(); | 139 var left = _parsePrimary(); |
| 216 while (scanner.hasMore() && scanner.current == "if") { | 140 while (_scanner.match(_Token.and)) { |
| 217 scanner.advance(); | 141 var right = _parsePrimary(); |
| 218 BooleanExpression right = parseBooleanExpression(); | 142 left = new _LogicExpression(_Token.and, left, right); |
| 219 left = new SetIf(left, right); | 143 } |
| 220 } | 144 |
| 221 return left; | 145 return left; |
| 222 } | 146 } |
| 223 | 147 |
| 224 SetExpression parseSetOr() { | 148 Expression _parsePrimary() { |
| 225 SetExpression left = parseSetAtomic(); | 149 if (_scanner.match(_Token.leftParen)) { |
| 226 while (scanner.hasMore() && scanner.current == Token.OR) { | 150 var value = _parseOr(); |
| 227 scanner.advance(); | 151 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"); | 152 throw new FormatException("Missing right parenthesis in expression"); |
| 240 } | 153 } |
| 241 scanner.advance(); | 154 |
| 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; | 155 return value; |
| 284 } | 156 } |
| 285 | 157 |
| 286 // The only atomic booleans are of the form $variable == value or | 158 // The only atomic booleans are of the form $variable == value or |
| 287 // of the form $variable. | 159 // of the form $variable. |
| 288 if (scanner.current != Token.DOLLAR_SYMBOL) { | 160 if (!_scanner.match(_Token.dollar)) { |
| 289 throw new FormatException( | 161 throw new FormatException( |
| 290 "Expected \$ in expression, got ${scanner.current}"); | 162 "Expected \$ in expression, got ${_scanner.current}"); |
| 291 } | 163 } |
| 292 scanner.advance(); | 164 |
| 293 if (!new RegExp(r"^\w+$").hasMatch(scanner.current)) { | 165 if (!_scanner.isIdentifier) { |
| 294 throw new FormatException( | 166 throw new FormatException( |
| 295 "Expected identifier in expression, got ${scanner.current}"); | 167 "Expected identifier in expression, got ${_scanner.current}"); |
| 296 } | 168 } |
| 297 TermVariable left = new TermVariable(scanner.current); | 169 |
| 298 scanner.advance(); | 170 var left = new _Variable(_scanner.current); |
| 299 if (scanner.current == Token.EQUALS || | 171 _scanner.advance(); |
| 300 scanner.current == Token.NOT_EQUALS) { | 172 |
| 301 bool negate = scanner.current == Token.NOT_EQUALS; | 173 if (_scanner.current == _Token.equals || |
| 302 scanner.advance(); | 174 _scanner.current == _Token.notEqual) { |
| 303 if (!new RegExp(r"^\w+$").hasMatch(scanner.current)) { | 175 var negate = _scanner.advance() == _Token.notEqual; |
| 176 |
| 177 if (!_scanner.isIdentifier) { |
| 304 throw new FormatException( | 178 throw new FormatException( |
| 305 "Expected value in expression, got ${scanner.current}"); | 179 "Expected value in expression, got ${_scanner.current}"); |
| 306 } | 180 } |
| 307 TermConstant right = new TermConstant(scanner.current); | 181 |
| 308 scanner.advance(); | 182 var right = _scanner.advance(); |
| 309 return new Comparison(left, right, negate); | 183 return new _ComparisonExpression(left, right, negate); |
| 310 } else { | 184 } else { |
| 311 return new BooleanVariable(left); | 185 return new _VariableExpression(left); |
| 312 } | 186 } |
| 313 } | 187 } |
| 314 } | 188 } |
| 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 |