d7ad4b99cd
This will be helpful when computing overlapping sections and other manipulations of status files. Bug: Change-Id: I8896e9684cc65bdd971451ab9b6cb39732984f68 Reviewed-on: https://dart-review.googlesource.com/23260 Commit-Queue: Morten Krogh-jespersen <mkroghj@google.com> Reviewed-by: William Hesse <whesse@google.com> Reviewed-by: Bob Nystrom <rnystrom@google.com>
450 lines
13 KiB
Dart
450 lines
13 KiB
Dart
// Copyright (c) 2017, 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.
|
|
|
|
import 'dart:collection';
|
|
import 'dart:math' as math;
|
|
|
|
import '../environment.dart';
|
|
|
|
/// A parsed Boolean expression AST.
|
|
abstract class Expression implements Comparable<Expression> {
|
|
/// Parses Boolean expressions in a .status file for Dart.
|
|
///
|
|
/// The grammar is:
|
|
///
|
|
/// expression := or
|
|
/// or := and ( "||" and )*
|
|
/// and := primary ( "&&" primary )*
|
|
/// primary := "$" identifier ( "==" | "!=" ) 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();
|
|
|
|
/// Validates that this expression does not contain any invalid uses of
|
|
/// variables.
|
|
///
|
|
/// Ensures that any variable names are known and that any literal values are
|
|
/// allowed for their corresponding variable. If an invalid variable or value
|
|
/// is found, adds appropriate error messages to [errors].
|
|
void validate(Environment environment, List<String> errors);
|
|
|
|
/// Evaluates the expression where all variables are defined by the given
|
|
/// [environment].
|
|
bool evaluate(Environment environment);
|
|
|
|
/// Produce a "normalized" version of this expression.
|
|
///
|
|
/// This removes any redundant computation and orders subexpressions in a
|
|
/// well-defined way such that two expressions with the same tree structure
|
|
/// and operands should result in equivalent expressions. It:
|
|
///
|
|
/// * Simplifies comparisons against boolean literals "true" and "false" to
|
|
/// the equivalent bare variable forms.
|
|
/// * Sorts the operands to a series of logic operators in a well-defined way.
|
|
/// (We are free to do this because status expressions are side-effect free
|
|
/// and don't need to short-circuit).
|
|
/// * Removes duplicate operands to logic operators.
|
|
///
|
|
/// This does not try to produce a *minimal* expression that calculates the
|
|
/// same truth values as the original expression.
|
|
Expression normalize();
|
|
|
|
/// Computes a relative ordering between two expressions or returns zero if
|
|
/// they are exactly identical.
|
|
///
|
|
/// This is useful for things like sorting lists of expressions or
|
|
/// normalizing a list of subexpressions. The rough logic is that higher
|
|
/// precedence and alphabetically lower expressions come first.
|
|
int compareTo(Expression other) {
|
|
var comparison = _typeComparison.compareTo(other._typeComparison);
|
|
if (comparison != 0) return comparison;
|
|
|
|
// They must be the same type.
|
|
return _compareToMyType(other);
|
|
}
|
|
|
|
int _compareToMyType(covariant Expression other);
|
|
|
|
/// The "precedence" of each expression type when comparing them using
|
|
/// `compareTo()`. Expressions whose type is lower compare earlier.
|
|
int get _typeComparison;
|
|
}
|
|
|
|
/// 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 = "||";
|
|
static const not = "!";
|
|
}
|
|
|
|
/// A reference to a variable.
|
|
class Variable {
|
|
final String name;
|
|
|
|
Variable(this.name);
|
|
|
|
String lookup(Environment environment) {
|
|
var value = environment.lookUp(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
|
|
/// ```
|
|
/// Negate the result if [negate] is true.
|
|
class ComparisonExpression extends Expression {
|
|
final Variable left;
|
|
final String right;
|
|
final bool negate;
|
|
|
|
ComparisonExpression(this.left, this.right, this.negate);
|
|
|
|
void validate(Environment environment, List<String> errors) {
|
|
environment.validate(left.name, right, errors);
|
|
}
|
|
|
|
bool evaluate(Environment environment) {
|
|
return negate != (left.lookup(environment) == right);
|
|
}
|
|
|
|
Expression normalize() {
|
|
// Replace Boolean comparisons with a straight variable expression.
|
|
if (right == "true") {
|
|
return new VariableExpression(left, negate: negate);
|
|
} else if (right == "false") {
|
|
return new VariableExpression(left, negate: !negate);
|
|
} else {
|
|
return this;
|
|
}
|
|
}
|
|
|
|
int _compareToMyType(ComparisonExpression other) {
|
|
if (left.name != other.left.name) {
|
|
return left.name.compareTo(other.left.name);
|
|
}
|
|
|
|
if (right != other.right) {
|
|
return right.compareTo(other.right);
|
|
}
|
|
|
|
return _compareBool(negate, other.negate);
|
|
}
|
|
|
|
// Comparisons come before variables so that "$compiler == ..." and
|
|
// "$runtime == ..." appear on the left in status expressions.
|
|
int get _typeComparison => 0;
|
|
|
|
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
|
|
/// ```
|
|
/// is equivalent to
|
|
/// ```
|
|
/// $variable == true
|
|
/// ```
|
|
/// Negates result if [negate] is true, so
|
|
/// ```
|
|
/// !$variable
|
|
/// ```
|
|
/// is equivalent to
|
|
/// ```
|
|
/// $variable != true
|
|
/// ```
|
|
class VariableExpression extends Expression {
|
|
final Variable variable;
|
|
final bool negate;
|
|
|
|
VariableExpression(this.variable, {this.negate = false});
|
|
|
|
void validate(Environment environment, List<String> errors) {
|
|
// It must be a Boolean, so it should allow either Boolean value.
|
|
environment.validate(variable.name, "true", errors);
|
|
}
|
|
|
|
bool evaluate(Environment environment) =>
|
|
negate != (variable.lookup(environment) == "true");
|
|
|
|
/// Variable expressions are fine as they are.
|
|
Expression normalize() => this;
|
|
|
|
int _compareToMyType(VariableExpression other) {
|
|
if (variable.name != other.variable.name) {
|
|
return variable.name.compareTo(other.variable.name);
|
|
}
|
|
|
|
return _compareBool(negate, other.negate);
|
|
}
|
|
|
|
int get _typeComparison => 1;
|
|
|
|
String toString() => "${negate ? "!" : ""}\$${variable.name}";
|
|
}
|
|
|
|
/// A logical `||` or `&&` expression.
|
|
class LogicExpression extends Expression {
|
|
/// The operator, `||` or `&&`.
|
|
final String op;
|
|
|
|
final List<Expression> operands;
|
|
|
|
LogicExpression(this.op, this.operands);
|
|
|
|
LogicExpression.and(this.operands) : op = _Token.and;
|
|
LogicExpression.or(this.operands) : op = _Token.or;
|
|
|
|
bool get isAnd => op == _Token.and;
|
|
bool get isOr => op == _Token.or;
|
|
|
|
void validate(Environment environment, List<String> errors) {
|
|
for (var operand in operands) {
|
|
operand.validate(environment, errors);
|
|
}
|
|
}
|
|
|
|
bool evaluate(Environment environment) {
|
|
if (op == _Token.and) {
|
|
return operands.every((operand) => operand.evaluate(environment));
|
|
} else {
|
|
return operands.any((operand) => operand.evaluate(environment));
|
|
}
|
|
}
|
|
|
|
Expression normalize() {
|
|
// Normalize the order of the clauses. Since there is no short-circuiting,
|
|
// a || b means the same as b || a. Picking a standard order lets us
|
|
// identify and collapse identical expressions that only differ by clause
|
|
// order.
|
|
|
|
// Recurse into the operands, sort them, and remove duplicates.
|
|
var normalized = new LogicExpression(
|
|
op, operands.map((operand) => operand.normalize()).toList())
|
|
.operands;
|
|
normalized = flatten(normalized);
|
|
var ordered = new SplayTreeSet<Expression>.from(normalized).toList();
|
|
return new LogicExpression(op, ordered);
|
|
}
|
|
|
|
List<Expression> flatten(List<Expression> operands) {
|
|
var newOperands = <Expression>[];
|
|
for (var operand in operands) {
|
|
if (operand is LogicExpression && operand.op == op) {
|
|
newOperands.addAll(operand.operands);
|
|
} else {
|
|
newOperands.add(operand);
|
|
}
|
|
}
|
|
return newOperands;
|
|
}
|
|
|
|
int _compareToMyType(LogicExpression other) {
|
|
// Put "&&" before "||".
|
|
if (op != other.op) return op == _Token.and ? -1 : 1;
|
|
|
|
// Lexicographically compare the operands.
|
|
var length = math.max(operands.length, other.operands.length);
|
|
for (var i = 0; i < length; i++) {
|
|
if (i >= operands.length) return -1;
|
|
if (i >= other.operands.length) return 1;
|
|
|
|
var comparison = operands[i].compareTo(other.operands[i]);
|
|
if (comparison != 0) return comparison;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int get _typeComparison => 2;
|
|
|
|
String toString() {
|
|
String parenthesize(Expression operand) {
|
|
var result = operand.toString();
|
|
if (isAnd && operand is LogicExpression && operand.isOr) {
|
|
result = "($result)";
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
return operands.map(parenthesize).join(" $op ");
|
|
}
|
|
}
|
|
|
|
/// 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 operands = [_parseAnd()];
|
|
while (_scanner.match(_Token.or)) {
|
|
operands.add(_parseAnd());
|
|
}
|
|
|
|
if (operands.length == 1) return operands.single;
|
|
|
|
return new LogicExpression(_Token.or, operands);
|
|
}
|
|
|
|
Expression _parseAnd() {
|
|
var operands = [_parsePrimary()];
|
|
while (_scanner.match(_Token.and)) {
|
|
operands.add(_parsePrimary());
|
|
}
|
|
|
|
if (operands.length == 1) return operands.single;
|
|
|
|
return new LogicExpression(_Token.and, operands);
|
|
}
|
|
|
|
Expression _parsePrimary() {
|
|
// TODO(mkroghj,rnystrom) If DNF is enforced, the need to parse parenthesis
|
|
// should go away. Remove this when all section headers has dnf'ed.
|
|
if (_scanner.match(_Token.leftParen)) {
|
|
var value = _parseOr();
|
|
if (!_scanner.match(_Token.rightParen)) {
|
|
throw new FormatException("Missing right parenthesis in expression");
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
var negate = false;
|
|
if (_scanner.match(_Token.not)) {
|
|
negate = true;
|
|
}
|
|
|
|
// 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 (!negate &&
|
|
(_scanner.current == _Token.equals ||
|
|
_scanner.current == _Token.notEqual)) {
|
|
var isNotEquals = _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, isNotEquals);
|
|
} else {
|
|
return new VariableExpression(left, negate: negate);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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+");
|
|
|
|
/// Pattern that recognizes identifier tokens.
|
|
///
|
|
/// Only checks the first character, since no non-identifier token can start
|
|
/// with a word character.
|
|
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.
|
|
// All non-identifier tokens are one or two characters,
|
|
// so a longer token must be an identifier.
|
|
bool get isIdentifier =>
|
|
current.length > 2 || _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;
|
|
}
|
|
}
|
|
|
|
int _compareBool(bool a, bool b) {
|
|
if (a == b) return 0;
|
|
if (a) return 1;
|
|
return -1;
|
|
}
|