| // Copyright 2014 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| package com.google.devtools.build.lib.syntax; |
| |
| import com.google.common.base.Strings; |
| import com.google.devtools.build.lib.events.Location; |
| import com.google.devtools.build.lib.syntax.Concatable.Concatter; |
| import com.google.devtools.build.lib.syntax.SkylarkList.MutableList; |
| import com.google.devtools.build.lib.syntax.SkylarkList.Tuple; |
| import java.io.IOException; |
| import java.util.EnumSet; |
| import java.util.IllegalFormatException; |
| |
| /** A BinaryExpression represents a binary operator expression 'x op y'. */ |
| public final class BinaryOperatorExpression extends Expression { |
| |
| private final Expression x; |
| private final TokenKind op; // one of 'operators' |
| private final Expression y; |
| |
| /** operators is the set of valid binary operators. */ |
| public static final EnumSet<TokenKind> operators = |
| EnumSet.of( |
| TokenKind.AND, |
| TokenKind.EQUALS_EQUALS, |
| TokenKind.GREATER, |
| TokenKind.GREATER_EQUALS, |
| TokenKind.IN, |
| TokenKind.LESS, |
| TokenKind.LESS_EQUALS, |
| TokenKind.MINUS, |
| TokenKind.NOT_EQUALS, |
| TokenKind.NOT_IN, |
| TokenKind.OR, |
| TokenKind.PERCENT, |
| TokenKind.SLASH, |
| TokenKind.SLASH_SLASH, |
| TokenKind.PLUS, |
| TokenKind.PIPE, |
| TokenKind.STAR); |
| |
| public BinaryOperatorExpression(Expression x, TokenKind op, Expression y) { |
| this.x = x; |
| this.op = op; |
| this.y = y; |
| } |
| |
| /** getX returns the left operand. */ |
| public Expression getX() { |
| return x; |
| } |
| |
| /** getOperator returns the operator. */ |
| public TokenKind getOperator() { |
| return op; |
| } |
| |
| /** getY returns the right operand. */ |
| public Expression getY() { |
| return y; |
| } |
| |
| @Override |
| public void prettyPrint(Appendable buffer) throws IOException { |
| // TODO(bazel-team): retain parentheses in the syntax tree so we needn't |
| // conservatively emit them here. |
| buffer.append('('); |
| x.prettyPrint(buffer); |
| buffer.append(' '); |
| buffer.append(op.toString()); |
| buffer.append(' '); |
| y.prettyPrint(buffer); |
| buffer.append(')'); |
| } |
| |
| @Override |
| public String toString() { |
| // This omits the parentheses for brevity, but is not correct in general due to operator |
| // precedence rules. |
| return x + " " + op + " " + y; |
| } |
| |
| /** Implements comparison operators. */ |
| private static int compare(Object x, Object y, Location location) throws EvalException { |
| try { |
| return EvalUtils.SKYLARK_COMPARATOR.compare(x, y); |
| } catch (EvalUtils.ComparisonException e) { |
| throw new EvalException(location, e); |
| } |
| } |
| |
| /** Implements 'x in y'. */ |
| private static boolean in(Object x, Object y, Environment env, Location location) |
| throws EvalException { |
| if (env.getSemantics().incompatibleDepsetIsNotIterable() && y instanceof SkylarkNestedSet) { |
| throw new EvalException( |
| location, |
| "argument of type '" |
| + EvalUtils.getDataTypeName(y) |
| + "' is not iterable. " |
| + "in operator only works on lists, tuples, dicts and strings. " |
| + "Use --incompatible_depset_is_not_iterable=false to temporarily disable " |
| + "this check."); |
| } else if (y instanceof SkylarkQueryable) { |
| return ((SkylarkQueryable) y).containsKey(x, location, env); |
| } else if (y instanceof String) { |
| if (x instanceof String) { |
| return ((String) y).contains((String) x); |
| } else { |
| throw new EvalException( |
| location, |
| "'in <string>' requires string as left operand, not '" |
| + EvalUtils.getDataTypeName(x) |
| + "'"); |
| } |
| } else { |
| throw new EvalException( |
| location, |
| "argument of type '" |
| + EvalUtils.getDataTypeName(y) |
| + "' is not iterable. " |
| + "in operator only works on lists, tuples, dicts and strings."); |
| } |
| } |
| |
| /** |
| * Evaluates a short-circuiting binary operator, i.e. boolean {@code and} or {@code or}. |
| * |
| * <p>In contrast to {@link #evaluate}, this method takes unevaluated expressions. The left-hand |
| * side expression is evaluated exactly once, and the right-hand side expression is evaluated |
| * either once or not at all. |
| * |
| * @throws IllegalArgumentException if {@code op} is not {@link Operator#AND} or {@link |
| * Operator#OR}. |
| */ |
| public static Object evaluateWithShortCircuiting( |
| TokenKind op, Expression x, Expression y, Environment env, Location loc) |
| throws EvalException, InterruptedException { |
| Object xval = x.eval(env); |
| switch (op) { |
| case AND: |
| return EvalUtils.toBoolean(xval) ? y.eval(env) : xval; |
| case OR: |
| return EvalUtils.toBoolean(xval) ? xval : y.eval(env); |
| default: |
| throw new IllegalArgumentException("Not a short-circuiting operator: " + op); |
| } |
| } |
| |
| /** |
| * Evaluates {@code x @ y}, where {@code @} is the operator, and returns the result. |
| * |
| * <p>This method does not implement any short-circuiting logic for boolean operations, as the |
| * parameters are already evaluated. |
| */ |
| public static Object evaluate(TokenKind op, Object x, Object y, Environment env, Location loc) |
| throws EvalException, InterruptedException { |
| return evaluate(op, x, y, env, loc, /*isAugmented=*/ false); |
| } |
| |
| private static Object evaluate( |
| TokenKind op, Object x, Object y, Environment env, Location location, boolean isAugmented) |
| throws EvalException, InterruptedException { |
| try { |
| switch (op) { |
| // AND and OR are included for completeness, but should normally be handled using |
| // evaluateWithShortCircuiting() instead of this method. |
| |
| case AND: |
| return EvalUtils.toBoolean(x) ? y : x; |
| |
| case OR: |
| return EvalUtils.toBoolean(x) ? x : y; |
| |
| case PLUS: |
| return plus(x, y, env, location, isAugmented); |
| |
| case PIPE: |
| return pipe(x, y, env, location); |
| |
| case MINUS: |
| return minus(x, y, location); |
| |
| case STAR: |
| return mult(x, y, env, location); |
| |
| case SLASH: |
| throw new EvalException( |
| location, |
| "The `/` operator is not allowed. Please use the `//` operator for integer " |
| + "division."); |
| |
| case SLASH_SLASH: |
| return divide(x, y, location); |
| |
| case PERCENT: |
| return percent(x, y, location); |
| |
| case EQUALS_EQUALS: |
| return x.equals(y); |
| |
| case NOT_EQUALS: |
| return !x.equals(y); |
| |
| case LESS: |
| return compare(x, y, location) < 0; |
| |
| case LESS_EQUALS: |
| return compare(x, y, location) <= 0; |
| |
| case GREATER: |
| return compare(x, y, location) > 0; |
| |
| case GREATER_EQUALS: |
| return compare(x, y, location) >= 0; |
| |
| case IN: |
| return in(x, y, env, location); |
| |
| case NOT_IN: |
| return !in(x, y, env, location); |
| |
| default: |
| throw new AssertionError("Unsupported binary operator: " + op); |
| } // endswitch |
| } catch (ArithmeticException e) { |
| throw new EvalException(location, e.getMessage()); |
| } |
| } |
| |
| /** |
| * Evaluates {@code x @= y} and returns the result, possibly mutating {@code x}. |
| * |
| * <p>Whether or not {@code x} is mutated depends on its type. If it is mutated, then it is also |
| * the return value. |
| */ |
| public static Object evaluateAugmented( |
| TokenKind op, Object x, Object y, Environment env, Location loc) |
| throws EvalException, InterruptedException { |
| return evaluate(op, x, y, env, loc, /*isAugmented=*/ true); |
| } |
| |
| @Override |
| Object doEval(Environment env) throws EvalException, InterruptedException { |
| if (op == TokenKind.AND || op == TokenKind.OR) { |
| return evaluateWithShortCircuiting(op, x, y, env, getLocation()); |
| } else { |
| return evaluate(op, x.eval(env), y.eval(env), env, getLocation()); |
| } |
| } |
| |
| @Override |
| public void accept(SyntaxTreeVisitor visitor) { |
| visitor.visit(this); |
| } |
| |
| @Override |
| public Kind kind() { |
| return Kind.BINARY_OPERATOR; |
| } |
| |
| /** Implements 'x + y'. */ |
| private static Object plus( |
| Object x, Object y, Environment env, Location location, boolean isAugmented) |
| throws EvalException { |
| // int + int |
| if (x instanceof Integer && y instanceof Integer) { |
| return Math.addExact((Integer) x, (Integer) y); |
| } |
| |
| // string + string |
| if (x instanceof String && y instanceof String) { |
| return (String) x + (String) y; |
| } |
| |
| if (x instanceof SelectorValue |
| || y instanceof SelectorValue |
| || x instanceof SelectorList |
| || y instanceof SelectorList) { |
| return SelectorList.concat(location, x, y); |
| } |
| |
| if (x instanceof Tuple && y instanceof Tuple) { |
| return Tuple.concat((Tuple<?>) x, (Tuple<?>) y); |
| } |
| |
| if (x instanceof MutableList && y instanceof MutableList) { |
| if (isAugmented) { |
| @SuppressWarnings("unchecked") |
| MutableList<Object> list = (MutableList) x; |
| list.addAll((MutableList<?>) y, location, env.mutability()); |
| return list; |
| } else { |
| return MutableList.concat((MutableList<?>) x, (MutableList<?>) y, env.mutability()); |
| } |
| } |
| |
| if (x instanceof SkylarkDict && y instanceof SkylarkDict) { |
| if (env.getSemantics().incompatibleDisallowDictPlus()) { |
| throw new EvalException( |
| location, |
| "The `+` operator for dicts is deprecated and no longer supported. Please use the " |
| + "`update` method instead. You can temporarily enable the `+` operator by passing " |
| + "the flag --incompatible_disallow_dict_plus=false"); |
| } |
| return SkylarkDict.plus((SkylarkDict<?, ?>) x, (SkylarkDict<?, ?>) y, env); |
| } |
| |
| if (x instanceof Concatable && y instanceof Concatable) { |
| Concatable lobj = (Concatable) x; |
| Concatable robj = (Concatable) y; |
| Concatter concatter = lobj.getConcatter(); |
| if (concatter != null && concatter.equals(robj.getConcatter())) { |
| return concatter.concat(lobj, robj, location); |
| } else { |
| throw typeException(x, y, TokenKind.PLUS, location); |
| } |
| } |
| |
| // TODO(bazel-team): Remove deprecated operator. |
| if (x instanceof SkylarkNestedSet) { |
| if (env.getSemantics().incompatibleDepsetUnion()) { |
| throw new EvalException( |
| location, |
| "`+` operator on a depset is forbidden. See " |
| + "https://docs.bazel.build/versions/master/skylark/depsets.html for " |
| + "recommendations. Use --incompatible_depset_union=false " |
| + "to temporarily disable this check."); |
| } |
| return SkylarkNestedSet.of((SkylarkNestedSet) x, y, location); |
| } |
| throw typeException(x, y, TokenKind.PLUS, location); |
| } |
| |
| /** Implements 'x | y'. */ |
| private static Object pipe(Object x, Object y, Environment env, Location location) |
| throws EvalException { |
| if (x instanceof SkylarkNestedSet) { |
| if (env.getSemantics().incompatibleDepsetUnion()) { |
| throw new EvalException( |
| location, |
| "`|` operator on a depset is forbidden. See " |
| + "https://docs.bazel.build/versions/master/skylark/depsets.html for " |
| + "recommendations. Use --incompatible_depset_union=false " |
| + "to temporarily disable this check."); |
| } |
| return SkylarkNestedSet.of((SkylarkNestedSet) x, y, location); |
| } |
| throw typeException(x, y, TokenKind.PIPE, location); |
| } |
| |
| /** Implements 'x - y'. */ |
| private static Object minus(Object x, Object y, Location location) throws EvalException { |
| if (x instanceof Integer && y instanceof Integer) { |
| return Math.subtractExact((Integer) x, (Integer) y); |
| } |
| throw typeException(x, y, TokenKind.MINUS, location); |
| } |
| |
| /** Implements 'x * y'. */ |
| private static Object mult(Object x, Object y, Environment env, Location location) |
| throws EvalException { |
| Integer number = null; |
| Object otherFactor = null; |
| |
| if (x instanceof Integer) { |
| number = (Integer) x; |
| otherFactor = y; |
| } else if (y instanceof Integer) { |
| number = (Integer) y; |
| otherFactor = x; |
| } |
| |
| if (number != null) { |
| if (otherFactor instanceof Integer) { |
| return Math.multiplyExact(number, (Integer) otherFactor); |
| } else if (otherFactor instanceof String) { |
| // Similar to Python, a factor < 1 leads to an empty string. |
| return Strings.repeat((String) otherFactor, Math.max(0, number)); |
| } else if (otherFactor instanceof SkylarkList && !(otherFactor instanceof RangeList)) { |
| // Similar to Python, a factor < 1 leads to an empty string. |
| return ((SkylarkList<?>) otherFactor).repeat(number, env.mutability()); |
| } |
| } |
| throw typeException(x, y, TokenKind.STAR, location); |
| } |
| |
| /** Implements 'x // y'. */ |
| private static Object divide(Object x, Object y, Location location) throws EvalException { |
| // int / int |
| if (x instanceof Integer && y instanceof Integer) { |
| if (y.equals(0)) { |
| throw new EvalException(location, "integer division by zero"); |
| } |
| // Integer division doesn't give the same result in Java and in Python 2 with |
| // negative numbers. |
| // Java: -7/3 = -2 |
| // Python: -7/3 = -3 |
| // We want to follow Python semantics, so we use float division and round down. |
| return (int) Math.floor(Double.valueOf((Integer) x) / (Integer) y); |
| } |
| throw typeException(x, y, TokenKind.SLASH_SLASH, location); |
| } |
| |
| /** Implements 'x % y'. */ |
| private static Object percent(Object x, Object y, Location location) throws EvalException { |
| // int % int |
| if (x instanceof Integer && y instanceof Integer) { |
| if (y.equals(0)) { |
| throw new EvalException(location, "integer modulo by zero"); |
| } |
| // Python and Java implement division differently, wrt negative numbers. |
| // In Python, sign of the result is the sign of the divisor. |
| int div = (Integer) y; |
| int result = ((Integer) x).intValue() % Math.abs(div); |
| if (result > 0 && div < 0) { |
| result += div; // make the result negative |
| } else if (result < 0 && div > 0) { |
| result += div; // make the result positive |
| } |
| return result; |
| } |
| |
| // string % tuple, string % dict, string % anything-else |
| if (x instanceof String) { |
| String pattern = (String) x; |
| try { |
| if (y instanceof Tuple) { |
| return Printer.formatWithList(pattern, (Tuple) y); |
| } |
| return Printer.format(pattern, y); |
| } catch (IllegalFormatException e) { |
| throw new EvalException(location, e.getMessage()); |
| } |
| } |
| throw typeException(x, y, TokenKind.PERCENT, location); |
| } |
| |
| /** Throws an exception signifying incorrect types for the given operator. */ |
| private static EvalException typeException(Object x, Object y, TokenKind op, Location location) { |
| // NB: this message format is identical to that used by CPython 2.7.6 or 3.4.0, |
| // though python raises a TypeError. |
| // For more details, we'll hopefully have usable stack traces at some point. |
| return new EvalException( |
| location, |
| String.format( |
| "unsupported operand type(s) for %s: '%s' and '%s'", |
| op, EvalUtils.getDataTypeName(x), EvalUtils.getDataTypeName(y))); |
| } |
| } |