Add support for binary operations.

This commit is contained in:
Natalie Weizenbaum 2016-09-19 08:50:39 -07:00 committed by Natalie Weizenbaum
parent 7fb7804a52
commit 9ac48fbc05
13 changed files with 370 additions and 27 deletions

View File

@ -96,8 +96,10 @@ official behavior.
indentation across the whole document. This doesn't have an issue yet; I need
to talk to Chris to determine if it's actually the right way forward.
6. Colors do not support channel-by-channel arithmetic. See [issue 2144][].
[issue 1599]: https://github.com/sass/sass/issues/1599
[issue 1126]: https://github.com/sass/sass/issues/1126
[issue 2120]: https://github.com/sass/sass/issues/2120
[issue 1122]: https://github.com/sass/sass/issues/1122
[issue 2144]: https://github.com/sass/sass/issues/2144

View File

@ -8,6 +8,7 @@ export 'sass/argument_invocation.dart';
export 'sass/callable_declaration.dart';
export 'sass/callable_invocation.dart';
export 'sass/expression.dart';
export 'sass/expression/binary_operation.dart';
export 'sass/expression/boolean.dart';
export 'sass/expression/color.dart';
export 'sass/expression/function.dart';

View File

@ -0,0 +1,77 @@
// Copyright 2016 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'package:source_span/source_span.dart';
import 'package:charcode/charcode.dart';
import '../../../utils.dart';
import '../../../visitor/interface/expression.dart';
import '../expression.dart';
class BinaryOperationExpression implements Expression {
final BinaryOperator operator;
final Expression left;
final Expression right;
FileSpan get span => spanForList([left, right]);
BinaryOperationExpression(this.operator, this.left, this.right);
/*=T*/ accept/*<T>*/(ExpressionVisitor/*<T>*/ visitor) =>
visitor.visitBinaryOperationExpression(this);
String toString() {
var buffer = new StringBuffer();
var left = this.left; // Hack to make analysis work.
var leftNeedsParens = left is BinaryOperationExpression &&
left.operator.precedence < operator.precedence;
if (leftNeedsParens) buffer.writeCharCode($lparen);
buffer.write(left);
if (leftNeedsParens) buffer.writeCharCode($rparen);
buffer.writeCharCode($space);
buffer.write(operator.operator);
buffer.writeCharCode($space);
var right = this.right; // Hack to make analysis work.
var rightNeedsParens = right is BinaryOperationExpression &&
right.operator.precedence <= operator.precedence;
if (rightNeedsParens) buffer.writeCharCode($lparen);
buffer.write(right);
if (rightNeedsParens) buffer.writeCharCode($rparen);
return buffer.toString();
}
}
class BinaryOperator {
static const or = const BinaryOperator._("or", "or", 0);
static const and = const BinaryOperator._("and", "and", 1);
static const equals = const BinaryOperator._("equals", "==", 2);
static const notEquals = const BinaryOperator._("not equals", "!=", 2);
static const greaterThan = const BinaryOperator._("greater than", ">", 3);
static const greaterThanOrEquals =
const BinaryOperator._("greater than or equals", ">=", 3);
static const lessThan = const BinaryOperator._("less than", "<", 3);
static const lessThanOrEquals =
const BinaryOperator._("less than or equals", "<=", 3);
static const plus = const BinaryOperator._("plus", "+", 4);
static const minus = const BinaryOperator._("minus", "-", 4);
static const times = const BinaryOperator._("times", "*", 5);
static const dividedBy = const BinaryOperator._("divided by", "/", 5);
static const modulo = const BinaryOperator._("modulo", "%", 5);
final String name;
final String operator;
final int precedence;
const BinaryOperator._(this.name, this.operator, this.precedence);
String toString() => name;
}

View File

@ -679,16 +679,63 @@ abstract class StylesheetParser extends Parser {
List<Expression> commaExpressions;
List<Expression> spaceExpressions;
// Operators whose right-hand operands are not fully parsed yet, in order of
// appearance in the document. Because a low-precedence operator will cause
// parsing to finish for all preceding higher-precedence operators, this is
// naturally ordered from lowest to highest precedence.
List<BinaryOperator> operators;
// The left-hand sides of [operators]. `operands[n]` is the left-hand side
// of `operators[n]`.
List<Expression> operands;
var singleExpression = _singleExpression();
resolveOneOperation() {
assert(singleExpression != null);
singleExpression = new BinaryOperationExpression(
operators.removeLast(), operands.removeLast(), singleExpression);
}
resolveOperations() {
if (operators == null) return;
while (!operators.isEmpty) {
resolveOneOperation();
}
}
addSingleExpression(Expression expression) {
if (singleExpression != null) {
spaceExpressions ??= [];
resolveOperations();
spaceExpressions.add(singleExpression);
}
singleExpression = expression;
}
addOperator(BinaryOperator operator) {
operators ??= [];
operands ??= [];
while (operators.isNotEmpty &&
operators.last.precedence >= operator.precedence) {
resolveOneOperation();
}
operators.add(operator);
assert(singleExpression != null);
operands.add(singleExpression);
singleExpression = null;
}
resolveSpaceExpressions() {
if (singleExpression != null) resolveOperations();
if (spaceExpressions == null) return;
if (singleExpression != null) spaceExpressions.add(singleExpression);
singleExpression =
new ListExpression(spaceExpressions, ListSeparator.space);
spaceExpressions = null;
}
loop:
while (true) {
whitespace();
@ -700,10 +747,6 @@ abstract class StylesheetParser extends Parser {
addSingleExpression(_parentheses());
break;
case $slash:
addSingleExpression(_unaryOperation());
break;
case $lbracket:
addSingleExpression(_bracketedList());
break;
@ -725,12 +768,61 @@ abstract class StylesheetParser extends Parser {
addSingleExpression(_hashExpression());
break;
case $equal:
scanner.readChar();
scanner.expectChar($equal);
addOperator(BinaryOperator.equals);
break;
case $exclamation:
scanner.readChar();
scanner.expectChar($equal);
addOperator(BinaryOperator.notEquals);
break;
case $langle:
scanner.readChar();
addOperator(scanner.scanChar($equal)
? BinaryOperator.lessThanOrEquals
: BinaryOperator.lessThan);
break;
case $rangle:
scanner.readChar();
addOperator(scanner.scanChar($equal)
? BinaryOperator.greaterThanOrEquals
: BinaryOperator.greaterThan);
break;
case $asterisk:
scanner.readChar();
addOperator(BinaryOperator.times);
break;
case $plus:
addSingleExpression(_plusExpression());
scanner.readChar();
addOperator(BinaryOperator.plus);
break;
case $minus:
addSingleExpression(_minusExpression());
var next = scanner.peekChar(1);
if (isDigit(next) || next == $dot) {
addSingleExpression(_number());
} else if (_lookingAtInterpolatedIdentifier()) {
addSingleExpression(_identifierLike());
} else {
addOperator(BinaryOperator.minus);
}
break;
case $slash:
scanner.readChar();
addOperator(BinaryOperator.dividedBy);
break;
case $percent:
scanner.readChar();
addOperator(BinaryOperator.modulo);
break;
case $0:
@ -748,6 +840,21 @@ abstract class StylesheetParser extends Parser {
break;
case $a:
if (scanIdentifier("and")) {
addOperator(BinaryOperator.and);
} else {
addSingleExpression(_identifierLike());
}
break;
case $o:
if (scanIdentifier("or")) {
addOperator(BinaryOperator.and);
} else {
addSingleExpression(_identifierLike());
}
break;
case $b:
case $c:
case $d:
@ -761,7 +868,6 @@ abstract class StylesheetParser extends Parser {
case $l:
case $m:
case $n:
case $o:
case $p:
case $q:
case $r:
@ -806,19 +912,12 @@ abstract class StylesheetParser extends Parser {
case $comma:
commaExpressions ??= [];
if (spaceExpressions != null) {
spaceExpressions.add(singleExpression);
commaExpressions
.add(new ListExpression(spaceExpressions, ListSeparator.space));
spaceExpressions = null;
singleExpression = null;
} else if (singleExpression != null) {
commaExpressions.add(singleExpression);
singleExpression = null;
} else {
scanner.error("Expected expression.");
}
if (singleExpression == null) scanner.error("Expected expression.");
resolveSpaceExpressions();
commaExpressions.add(singleExpression);
scanner.readChar();
singleExpression = null;
break;
default:
@ -835,16 +934,12 @@ abstract class StylesheetParser extends Parser {
}
}
if (spaceExpressions != null) {
if (singleExpression != null) spaceExpressions.add(singleExpression);
singleExpression =
new ListExpression(spaceExpressions, ListSeparator.space);
}
resolveSpaceExpressions();
if (commaExpressions != null) {
if (singleExpression != null) commaExpressions.add(singleExpression);
return new ListExpression(commaExpressions, ListSeparator.comma);
} else {
assert(singleExpression != null);
return singleExpression;
}
}

View File

@ -5,6 +5,7 @@
import 'exception.dart';
import 'value/boolean.dart';
import 'value/identifier.dart';
import 'value/string.dart';
import 'visitor/interface/value.dart';
import 'visitor/serialize.dart';
@ -31,6 +32,42 @@ abstract class Value {
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ visitor);
Value or(Value other) => this;
Value and(Value other) => other;
SassBoolean greaterThan(Value other) =>
throw new InternalException('Undefined operation "$this > $other".');
SassBoolean greaterThanOrEquals(Value other) =>
throw new InternalException('Undefined operation "$this >= $other".');
SassBoolean lessThan(Value other) =>
throw new InternalException('Undefined operation "$this < $other".');
SassBoolean lessThanOrEquals(Value other) =>
throw new InternalException('Undefined operation "$this <= $other".');
Value times(Value other) =>
throw new InternalException('Undefined operation "$this * $other".');
Value modulo(Value other) =>
throw new InternalException('Undefined operation "$this % $other".');
Value plus(Value other) {
if (other is SassString) {
return new SassString(valueToCss(this) + other.text);
} else {
return new SassIdentifier(valueToCss(this) + valueToCss(other));
}
}
Value minus(Value other) =>
new SassIdentifier("${valueToCss(this)}-${valueToCss(other)}");
Value dividedBy(Value other) =>
new SassIdentifier("${valueToCss(this)}/${valueToCss(other)}");
Value unaryPlus() => new SassIdentifier("+${valueToCss(this)}");
Value unaryMinus() => new SassIdentifier("-${valueToCss(this)}");

View File

@ -20,5 +20,9 @@ class SassBoolean extends Value {
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ visitor) =>
visitor.visitBoolean(this);
Value or(Value other) => value ? this : other;
Value and(Value other) => value ? other : this;
Value unaryNot() => value ? sassFalse : sassTrue;
}

View File

@ -2,6 +2,7 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import '../exception.dart';
import '../visitor/interface/value.dart';
import '../value.dart';
@ -16,6 +17,26 @@ class SassColor extends Value {
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ visitor) => visitor.visitColor(this);
Value plus(Value other) {
if (other is! SassNumber && other is! SassColor) return super.plus(other);
throw new InternalException('Undefined operation "$this + $other".');
}
Value minus(Value other) {
if (other is! SassNumber && other is! SassColor) return super.minus(other);
throw new InternalException('Undefined operation "$this - $other".');
}
Value dividedBy(Value other) {
if (other is! SassNumber && other is! SassColor) {
return super.dividedBy(other);
}
throw new InternalException('Undefined operation "$this / $other".');
}
Value modulo(Value other) =>
throw new InternalException('Undefined operation "$this % $other".');
bool operator ==(other) =>
other is SassColor &&
other.red == red &&

View File

@ -15,7 +15,11 @@ class SassIdentifier extends Value {
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ visitor) =>
visitor.visitIdentifier(this);
bool operator ==(other) => other is SassIdentifier && other.text == text;
bool operator ==(other) {
if (other is SassString) return text == other.text;
if (other is SassIdentifier) return text == other.text;
return false;
}
int get hashCode => text.hashCode;
}

View File

@ -14,5 +14,9 @@ class SassNull extends Value {
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ visitor) => visitor.visitNull(this);
Value or(Value other) => other;
Value and(Value other) => this;
Value unaryNot() => sassTrue;
}

View File

@ -24,6 +24,54 @@ class SassNumber extends Value {
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ visitor) =>
visitor.visitNumber(this);
SassBoolean greaterThan(Value other) {
if (other is SassNumber) return new SassBoolean(value > other.value);
throw new InternalException('Undefined operation "$this > $other".');
}
SassBoolean greaterThanOrEquals(Value other) {
if (other is SassNumber) return new SassBoolean(value >= other.value);
throw new InternalException('Undefined operation "$this >= $other".');
}
SassBoolean lessThan(Value other) {
if (other is SassNumber) return new SassBoolean(value < other.value);
throw new InternalException('Undefined operation "$this < $other".');
}
SassBoolean lessThanOrEquals(Value other) {
if (other is SassNumber) return new SassBoolean(value <= other.value);
throw new InternalException('Undefined operation "$this <= $other".');
}
Value times(Value other) {
if (other is SassNumber) return new SassNumber(value * other.value);
throw new InternalException('Undefined operation "$this * $other".');
}
Value modulo(Value other) {
if (other is SassNumber) return new SassNumber(value % other.value);
throw new InternalException('Undefined operation "$this % $other".');
}
Value plus(Value other) {
if (other is SassNumber) return new SassNumber(value + other.value);
if (other is! SassColor) return super.plus(other);
throw new InternalException('Undefined operation "$this + $other".');
}
Value minus(Value other) {
if (other is SassNumber) return new SassNumber(value - other.value);
if (other is! SassColor) return super.minus(other);
throw new InternalException('Undefined operation "$this - $other".');
}
Value dividedBy(Value other) {
if (other is SassNumber) return new SassNumber(value / other.value);
if (other is! SassColor) super.dividedBy(other);
throw new InternalException('Undefined operation "$this / $other".');
}
Value unaryPlus() => this;
Value unaryMinus() => new SassNumber(-value);

View File

@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT.
import '../visitor/interface/value.dart';
import '../visitor/serialize.dart';
import '../value.dart';
class SassString extends Value {
@ -12,4 +13,15 @@ class SassString extends Value {
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ visitor) =>
visitor.visitString(this);
Value plus(Value other) => new SassString(
text + (other is SassString ? other.text : valueToCss(other)));
bool operator ==(other) {
if (other is SassString) return text == other.text;
if (other is SassIdentifier) return text == other.text;
return false;
}
int get hashCode => text.hashCode;
}

View File

@ -5,6 +5,7 @@
import '../../ast/sass.dart';
abstract class ExpressionVisitor<T> {
T visitBinaryOperationExpression(BinaryOperationExpression node);
T visitBooleanExpression(BooleanExpression node);
T visitColorExpression(ColorExpression node);
T visitFunctionExpression(FunctionExpression node);

View File

@ -570,6 +570,43 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
// ## Expressions
Value visitBinaryOperationExpression(BinaryOperationExpression node) {
return _addExceptionSpan(() {
var left = node.left.accept(this);
var right = node.right.accept(this);
switch (node.operator) {
case BinaryOperator.or:
return left.or(right);
case BinaryOperator.and:
return left.and(right);
case BinaryOperator.equals:
return new SassBoolean(left == right);
case BinaryOperator.notEquals:
return new SassBoolean(left != right);
case BinaryOperator.greaterThan:
return left.greaterThan(right);
case BinaryOperator.greaterThanOrEquals:
return left.greaterThanOrEquals(right);
case BinaryOperator.lessThan:
return left.lessThan(right);
case BinaryOperator.lessThanOrEquals:
return left.lessThanOrEquals(right);
case BinaryOperator.plus:
return left.plus(right);
case BinaryOperator.minus:
return left.minus(right);
case BinaryOperator.times:
return left.times(right);
case BinaryOperator.dividedBy:
return left.dividedBy(right);
case BinaryOperator.modulo:
return left.modulo(right);
default:
return null;
}
}, node.span);
}
Value visitVariableExpression(VariableExpression node) {
var result = _environment.getVariable(node.name);
if (result != null) return result;