// 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.common.collect.Iterables; 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.util.Collections; import java.util.IllegalFormatException; /** * Syntax node for a binary operator expression. */ public final class BinaryOperatorExpression extends Expression { private final Expression lhs; private final Expression rhs; private final Operator operator; public BinaryOperatorExpression(Operator operator, Expression lhs, Expression rhs) { this.lhs = lhs; this.rhs = rhs; this.operator = operator; } public Expression getLhs() { return lhs; } public Expression getRhs() { return rhs; } /** * Returns the operator kind for this binary operation. */ public Operator getOperator() { return operator; } @Override public String toString() { return lhs + " " + operator + " " + rhs; } /** Implements comparison operators. */ private static int compare(Object lval, Object rval, Location location) throws EvalException { try { return EvalUtils.SKYLARK_COMPARATOR.compare(lval, rval); } catch (EvalUtils.ComparisonException e) { throw new EvalException(location, e); } } /** Implements the "in" operator. */ private static boolean in(Object lval, Object rval, Location location) throws EvalException { if (rval instanceof SkylarkQueryable) { return ((SkylarkQueryable) rval).containsKey(lval, location); } else if (rval instanceof String) { if (lval instanceof String) { return ((String) rval).contains((String) lval); } else { throw new EvalException( location, "'in <string>' requires string as left operand, not '" + EvalUtils.getDataTypeName(lval) + "'"); } } else { throw new EvalException( location, "argument of type '" + EvalUtils.getDataTypeName(rval) + "' is not iterable. " + "in operator only works on lists, tuples, dicts and strings."); } } /** Helper method. Reused from the LValue class. */ public static Object evaluate( Operator operator, Object lval, Expression rhs, Environment env, Location location, boolean isAugmented) throws EvalException, InterruptedException { // Short-circuit operators if (operator == Operator.AND) { if (EvalUtils.toBoolean(lval)) { return rhs.eval(env); } else { return lval; } } if (operator == Operator.OR) { if (EvalUtils.toBoolean(lval)) { return lval; } else { return rhs.eval(env); } } Object rval = rhs.eval(env); switch (operator) { case PLUS: return plus(lval, rval, env, location, isAugmented); case PIPE: return pipe(lval, rval, location); case MINUS: return minus(lval, rval, location); case MULT: return mult(lval, rval, env, location); case DIVIDE: return divide(lval, rval, location); case PERCENT: return percent(lval, rval, location); case EQUALS_EQUALS: return lval.equals(rval); case NOT_EQUALS: return !lval.equals(rval); case LESS: return compare(lval, rval, location) < 0; case LESS_EQUALS: return compare(lval, rval, location) <= 0; case GREATER: return compare(lval, rval, location) > 0; case GREATER_EQUALS: return compare(lval, rval, location) >= 0; case IN: return in(lval, rval, location); case NOT_IN: return !in(lval, rval, location); default: throw new AssertionError("Unsupported binary operator: " + operator); } // endswitch } @Override Object doEval(Environment env) throws EvalException, InterruptedException { return evaluate(operator, lhs.eval(env), rhs, env, getLocation(), false); } @Override public void accept(SyntaxTreeVisitor visitor) { visitor.visit(this); } @Override void validate(ValidationEnvironment env) throws EvalException { lhs.validate(env); rhs.validate(env); } /** Implements Operator.PLUS. */ private static Object plus( Object lval, Object rval, Environment env, Location location, boolean isAugmented) throws EvalException { // int + int if (lval instanceof Integer && rval instanceof Integer) { return ((Integer) lval).intValue() + ((Integer) rval).intValue(); } // string + string if (lval instanceof String && rval instanceof String) { return (String) lval + (String) rval; } if (lval instanceof SelectorValue || rval instanceof SelectorValue || lval instanceof SelectorList || rval instanceof SelectorList) { return SelectorList.concat(location, lval, rval); } if ((lval instanceof Tuple) && (rval instanceof Tuple)) { return Tuple.copyOf(Iterables.concat((Tuple) lval, (Tuple) rval)); } if ((lval instanceof MutableList) && (rval instanceof MutableList)) { if (isAugmented && env.getSemantics().incompatibleListPlusEqualsInplace) { @SuppressWarnings("unchecked") MutableList<Object> list = (MutableList) lval; list.addAll((MutableList<?>) rval, location, env); return list; } return MutableList.concat((MutableList) lval, (MutableList) rval, env); } if (lval instanceof SkylarkDict && rval 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<?, ?>) lval, (SkylarkDict<?, ?>) rval, env); } if (lval instanceof Concatable && rval instanceof Concatable) { Concatable lobj = (Concatable) lval; Concatable robj = (Concatable) rval; Concatter concatter = lobj.getConcatter(); if (concatter != null && concatter.equals(robj.getConcatter())) { return concatter.concat(lobj, robj, location); } else { throw typeException(lval, rval, Operator.PLUS, location); } } // TODO(bazel-team): Remove this case. Union of sets should use '|' instead of '+'. if (lval instanceof SkylarkNestedSet) { return new SkylarkNestedSet((SkylarkNestedSet) lval, rval, location); } throw typeException(lval, rval, Operator.PLUS, location); } /** Implements Operator.PIPE. */ private static Object pipe(Object lval, Object rval, Location location) throws EvalException { if (lval instanceof SkylarkNestedSet) { return new SkylarkNestedSet((SkylarkNestedSet) lval, rval, location); } throw typeException(lval, rval, Operator.PIPE, location); } /** Implements Operator.MINUS. */ private static Object minus(Object lval, Object rval, Location location) throws EvalException { if (lval instanceof Integer && rval instanceof Integer) { return ((Integer) lval).intValue() - ((Integer) rval).intValue(); } throw typeException(lval, rval, Operator.MINUS, location); } /** Implements Operator.MULT. */ private static Object mult(Object lval, Object rval, Environment env, Location location) throws EvalException { Integer number = null; Object otherFactor = null; if (lval instanceof Integer) { number = (Integer) lval; otherFactor = rval; } else if (rval instanceof Integer) { number = (Integer) rval; otherFactor = lval; } if (number != null) { if (otherFactor instanceof Integer) { return number.intValue() * ((Integer) otherFactor).intValue(); } 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.intValue())); } else if (otherFactor instanceof MutableList) { // Similar to Python, a factor < 1 leads to an empty string. return MutableList.duplicate( (MutableList<?>) otherFactor, Math.max(0, number.intValue()), env); } } throw typeException(lval, rval, Operator.MULT, location); } /** Implements Operator.DIVIDE. */ private static Object divide(Object lval, Object rval, Location location) throws EvalException { // int / int if (lval instanceof Integer && rval instanceof Integer) { if (rval.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) lval) / (Integer) rval); } throw typeException(lval, rval, Operator.DIVIDE, location); } /** Implements Operator.PERCENT. */ private static Object percent(Object lval, Object rval, Location location) throws EvalException { // int % int if (lval instanceof Integer && rval instanceof Integer) { if (rval.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) rval; int result = ((Integer) lval).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 (lval instanceof String) { String pattern = (String) lval; try { if (rval instanceof Tuple) { return Printer.formatToString(pattern, (Tuple) rval); } return Printer.formatToString(pattern, Collections.singletonList(rval)); } catch (IllegalFormatException e) { throw new EvalException(location, e.getMessage()); } } throw typeException(lval, rval, Operator.PERCENT, location); } /** * Throws an exception signifying incorrect types for the given operator. */ private static final EvalException typeException( Object lval, Object rval, Operator operator, 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'", operator, EvalUtils.getDataTypeName(lval), EvalUtils.getDataTypeName(rval))); } }