blob: c0e35c2017d050fe45c419e60f34b796df9d8d6f [file] [log] [blame]
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// A parsed Boolean expression AST.
abstract class Expression {
/// Parses Boolean expressions in a .status file for Dart.
///
/// The grammar is:
///
/// expression := or
/// or := and ( "||" and )*
/// and := primary ( "&&" primary )*
/// primary := "$" identifier ( ( "==" | "!=" ) identifier )? |
/// "(" expression ")"
/// identifier := regex "\w+"
///
/// Expressions evaluate as expected, with values of variables found in an
/// environment passed to the evaluator.
static Expression parse(String expression) =>
new _ExpressionParser(expression).parse();
/// Evaluates the expression where all variables are defined by the given
/// [environment].
bool evaluate(Map<String, dynamic> environment);
}
/// Keyword token strings.
class _Token {
static const leftParen = "(";
static const rightParen = ")";
static const dollar = r"$";
static const equals = "==";
static const notEqual = "!=";
static const and = "&&";
static const or = "||";
}
/// A reference to a variable.
class _Variable {
final String name;
_Variable(this.name);
String lookup(Map<String, dynamic> environment) {
var value = environment[name];
if (value == null) {
throw new Exception("Could not find '$name' in environment "
"while evaluating status file expression.");
}
// Explicitly stringify all values so that things like:
//
// $strong == true
//
// work correctly even though "true" is treated as a string here.
// TODO(rnystrom): Is there a cleaner/safer way to do this?
return value.toString();
}
}
/// Tests whether a given variable is or is not equal some literal value, as in:
///
/// $variable == someValue
class _ComparisonExpression implements Expression {
final _Variable left;
final String right;
final bool negate;
_ComparisonExpression(this.left, this.right, this.negate);
bool evaluate(Map<String, dynamic> environment) {
return negate != (left.lookup(environment) == right);
}
String toString() => "(\$${left.name} ${negate ? '!=' : '=='} $right)";
}
/// A reference to a variable defined in the environment. The expression
/// evaluates to true if the variable's stringified value is "true".
///
/// $variable
class _VariableExpression implements Expression {
final _Variable variable;
_VariableExpression(this.variable);
bool evaluate(Map<String, dynamic> environment) =>
variable.lookup(environment) == "true";
String toString() => "(bool \$${variable.name})";
}
/// A logical `||` or `&&` expression.
class _LogicExpression implements Expression {
/// The operator, `||` or `&&`.
final String op;
final Expression left;
final Expression right;
_LogicExpression(this.op, this.left, this.right);
bool evaluate(Map<String, dynamic> environment) => (op == _Token.and)
? left.evaluate(environment) && right.evaluate(environment)
: left.evaluate(environment) || right.evaluate(environment);
String toString() => "($left $op $right)";
}
/// Parser for Boolean expressions in a .status file for Dart.
class _ExpressionParser {
final _Scanner _scanner;
_ExpressionParser(String expression) : _scanner = new _Scanner(expression);
Expression parse() {
var expression = _parseOr();
// Should consume entire string.
if (_scanner.hasMore) {
throw new FormatException("Unexpected input after expression");
}
return expression;
}
Expression _parseOr() {
var left = _parseAnd();
while (_scanner.match(_Token.or)) {
var right = _parseAnd();
left = new _LogicExpression(_Token.or, left, right);
}
return left;
}
Expression _parseAnd() {
var left = _parsePrimary();
while (_scanner.match(_Token.and)) {
var right = _parsePrimary();
left = new _LogicExpression(_Token.and, left, right);
}
return left;
}
Expression _parsePrimary() {
if (_scanner.match(_Token.leftParen)) {
var value = _parseOr();
if (!_scanner.match(_Token.rightParen)) {
throw new FormatException("Missing right parenthesis in expression");
}
return value;
}
// The only atomic booleans are of the form $variable == value or
// of the form $variable.
if (!_scanner.match(_Token.dollar)) {
throw new FormatException(
"Expected \$ in expression, got ${_scanner.current}");
}
if (!_scanner.isIdentifier) {
throw new FormatException(
"Expected identifier in expression, got ${_scanner.current}");
}
var left = new _Variable(_scanner.current);
_scanner.advance();
if (_scanner.current == _Token.equals ||
_scanner.current == _Token.notEqual) {
var negate = _scanner.advance() == _Token.notEqual;
if (!_scanner.isIdentifier) {
throw new FormatException(
"Expected value in expression, got ${_scanner.current}");
}
var right = _scanner.advance();
return new _ComparisonExpression(left, right, negate);
} else {
return new _VariableExpression(left);
}
}
}
/// An iterator that allows peeking at the current token.
class _Scanner {
/// Tokens are "(", ")", "$", "&&", "||", "==", "!=", and (maximal) \w+.
static final _testPattern =
new RegExp(r"^([()$\w\s]|(\&\&)|(\|\|)|(\=\=)|(\!\=))+$");
static final _tokenPattern =
new RegExp(r"[()$]|(\&\&)|(\|\|)|(\=\=)|(\!\=)|\w+");
static final _identifierPattern = new RegExp(r"^\w+$");
/// The token strings being iterated.
final Iterator<String> tokenIterator;
String current;
_Scanner(String expression) : tokenIterator = tokenize(expression).iterator {
advance();
}
static List<String> tokenize(String expression) {
if (!_testPattern.hasMatch(expression)) {
throw new FormatException("Syntax error in '$expression'");
}
return _tokenPattern
.allMatches(expression)
.map((match) => match[0])
.toList();
}
bool get hasMore => current != null;
/// Returns `true` if the current token is an identifier.
bool get isIdentifier => _identifierPattern.hasMatch(current);
/// If the current token is [token], consumes it and returns `true`.
bool match(String token) {
if (!hasMore || current != token) return false;
advance();
return true;
}
/// Consumes the current token and returns it.
String advance() {
var previous = current;
current = tokenIterator.moveNext() ? tokenIterator.current : null;
return previous;
}
}