bazel syntax: generalize Concatter to HasBinary, for binary operators
A Starlark value that implements HasBinary may define its own
semantics for binary operators such as x+y. See HasBinary for details.
This brings us a step closer to moving Selector{List,Value} to lib.packages.
Also:
- remove the Location parameter from the operator method.
This API simplification causes a minor usability regression:
SkylarkInfo instances created by x+y will no longer record the
location of the plus operator.
- make the control tree for binary operator cases shallower
(that is, avoid unnecessary re-tests of instanceof),
with a single fall-through.
- The specialized error message for 'in' is no longer feasible
as the set of valid operand types is no longer closed.
- Simplify arithmetic division, borrowing from go.starlark.net.
This is a breaking API change for Copybara.
PiperOrigin-RevId: 291726807
diff --git a/src/main/java/com/google/devtools/build/lib/packages/BuildType.java b/src/main/java/com/google/devtools/build/lib/packages/BuildType.java
index 55b3bac..59f567f 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/BuildType.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/BuildType.java
@@ -568,7 +568,7 @@
selectorValueList.add(new SelectorValue(element.getEntries(), element.getNoMatchError()));
}
try {
- printer.repr(com.google.devtools.build.lib.syntax.SelectorList.of(null, selectorValueList));
+ printer.repr(com.google.devtools.build.lib.syntax.SelectorList.of(selectorValueList));
} catch (EvalException e) {
throw new IllegalStateException("this list should have been validated on creation");
}
diff --git a/src/main/java/com/google/devtools/build/lib/packages/SkylarkInfo.java b/src/main/java/com/google/devtools/build/lib/packages/SkylarkInfo.java
index 77ca87d..c08afdd 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/SkylarkInfo.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/SkylarkInfo.java
@@ -19,17 +19,18 @@
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.syntax.ClassObject;
-import com.google.devtools.build.lib.syntax.Concatable;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.EvalUtils;
+import com.google.devtools.build.lib.syntax.HasBinary;
import com.google.devtools.build.lib.syntax.Starlark;
+import com.google.devtools.build.lib.syntax.TokenKind;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/** An Info (provider instance) for providers defined in Starlark. */
-public final class SkylarkInfo extends StructImpl implements Concatable, ClassObject {
+public final class SkylarkInfo extends StructImpl implements HasBinary, ClassObject {
// For a n-element info, the table contains n key strings, sorted,
// followed by the n corresponding legal Starlark values.
@@ -187,23 +188,22 @@
}
@Override
- public Concatter getConcatter() {
- return SkylarkInfo::concat;
+ public SkylarkInfo binaryOp(TokenKind op, Object that, boolean thisLeft) throws EvalException {
+ if (op == TokenKind.PLUS && that instanceof SkylarkInfo) {
+ return thisLeft
+ ? plus(this, (SkylarkInfo) that) //
+ : plus((SkylarkInfo) that, this);
+ }
+ return null;
}
- private static Concatable concat(Concatable left, Concatable right, Location loc)
- throws EvalException {
- // Casts are safe because this Concatter is only used by SkylarkInfo.
- SkylarkInfo x = (SkylarkInfo) left;
- SkylarkInfo y = (SkylarkInfo) right;
+ private static SkylarkInfo plus(SkylarkInfo x, SkylarkInfo y) throws EvalException {
Provider xprov = x.getProvider();
Provider yprov = y.getProvider();
if (!xprov.equals(yprov)) {
- throw new EvalException(
- loc,
- String.format(
- "Cannot use '+' operator on instances of different providers (%s and %s)",
- xprov.getPrintableName(), yprov.getPrintableName()));
+ throw Starlark.errorf(
+ "Cannot use '+' operator on instances of different providers (%s and %s)",
+ xprov.getPrintableName(), yprov.getPrintableName());
}
// ztable = merge(x.table, y.table)
@@ -227,8 +227,7 @@
ztable[zi + zsize] = y.table[yi + ysize];
yi++;
} else {
- throw new EvalException(
- loc, String.format("cannot add struct instances with common field '%s'", xk));
+ throw Starlark.errorf("cannot add struct instances with common field '%s'", xk);
}
zi++;
}
@@ -245,6 +244,6 @@
zi++;
}
- return new SkylarkInfo(xprov, ztable, loc, x.unknownFieldError);
+ return new SkylarkInfo(xprov, ztable, Location.BUILTIN, x.unknownFieldError);
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/BUILD b/src/main/java/com/google/devtools/build/lib/syntax/BUILD
index e2bc985..1f9ec0d 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/syntax/BUILD
@@ -94,7 +94,6 @@
"CallUtils.java",
"Callstack.java",
"ClassObject.java",
- "Concatable.java",
"Debug.java",
"Debugger.java",
"Depset.java",
@@ -105,6 +104,7 @@
"EvalUtils.java",
"FlagGuardedValue.java",
"FormatParser.java",
+ "HasBinary.java",
"MethodDescriptor.java",
"MethodLibrary.java",
"Module.java",
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Concatable.java b/src/main/java/com/google/devtools/build/lib/syntax/Concatable.java
deleted file mode 100644
index 81f489f..0000000
--- a/src/main/java/com/google/devtools/build/lib/syntax/Concatable.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright 2016 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.devtools.build.lib.events.Location;
-import javax.annotation.Nullable;
-
-/**
- * Skylark values that support '+' operator should implement this interface.
- */
-public interface Concatable {
-
- /**
- * Implements 'plus' operator on ClassObjects.
- */
- interface Concatter {
- Concatable concat(Concatable lval, Concatable rval, Location loc) throws EvalException;
- }
-
- /* Returns a concatter for this {@link Concatable}.
- * Two {@link Concatable}s can be added together if their concatters are equal.
- */
- @Nullable
- Concatter getConcatter();
-}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Eval.java b/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
index 7c2cbd2..2a4f6c6 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Eval.java
@@ -390,15 +390,13 @@
Expression lhs = stmt.getLHS();
TokenKind op = stmt.getOperator();
Expression rhs = stmt.getRHS();
- // TODO(adonovan): don't materialize Locations before an error has occurred.
- // (Requires syntax tree to record offsets and defer Location conversion.)
- Location loc = stmt.getStartLocation(); // TODO(adonovan): use operator location
if (lhs instanceof Identifier) {
Object x = eval(fr, lhs);
Object y = eval(fr, rhs);
- Object z = inplaceBinaryOp(fr, op, x, y, loc);
+ Object z = inplaceBinaryOp(fr, op, x, y);
assignIdentifier(fr, (Identifier) lhs, z);
+
} else if (lhs instanceof IndexExpression) {
// object[index] op= y
// The object and key should be evaluated only once, so we don't use lhs.eval().
@@ -408,23 +406,27 @@
Object x = EvalUtils.index(fr.thread.mutability(), fr.thread.getSemantics(), object, key);
// Evaluate rhs after lhs.
Object y = eval(fr, rhs);
- Object z = inplaceBinaryOp(fr, op, x, y, loc);
+ Object z = inplaceBinaryOp(fr, op, x, y);
try {
assignItem(object, key, z);
} catch (EvalException ex) {
+ Location loc = stmt.getStartLocation(); // TODO(adonovan): use operator location
throw ex.ensureLocation(loc);
}
+
} else if (lhs instanceof ListExpression) {
+ Location loc = stmt.getStartLocation(); // TODO(adonovan): use operator location
throw new EvalException(loc, "cannot perform augmented assignment on a list literal");
+
} else {
// Not possible for validated ASTs.
+ Location loc = stmt.getStartLocation(); // TODO(adonovan): use operator location
throw new EvalException(loc, "cannot perform augmented assignment on '" + lhs + "'");
}
}
private static Object inplaceBinaryOp(
- StarlarkThread.CallFrame fr, TokenKind op, Object x, Object y, Location location)
- throws EvalException {
+ StarlarkThread.CallFrame fr, TokenKind op, Object x, Object y) throws EvalException {
// list += iterable behaves like list.extend(iterable)
// TODO(b/141263526): following Python, allow list+=iterable (but not list+iterable).
if (op == TokenKind.PLUS && x instanceof StarlarkList && y instanceof StarlarkList) {
@@ -432,7 +434,7 @@
list.extend(y);
return list;
}
- return EvalUtils.binaryOp(op, x, y, fr.thread, location);
+ return EvalUtils.binaryOp(op, x, y, fr.thread.getSemantics(), fr.thread.mutability());
}
// ---- expressions ----
@@ -474,9 +476,14 @@
return Starlark.truth(x) ? x : eval(fr, binop.getY());
default:
Object y = eval(fr, binop.getY());
- // TODO(adonovan): use operator location
- return EvalUtils.binaryOp(
- binop.getOperator(), x, y, fr.thread, binop.getStartLocation());
+ try {
+ return EvalUtils.binaryOp(
+ binop.getOperator(), x, y, fr.thread.getSemantics(), fr.thread.mutability());
+ } catch (EvalException ex) {
+ // TODO(adonovan): use operator location
+ ex.ensureLocation(binop.getStartLocation());
+ throw ex;
+ }
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
index 4e1b680..88cc999 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
@@ -21,7 +21,6 @@
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.skylarkinterface.SkylarkInterfaceUtils;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
-import com.google.devtools.build.lib.syntax.Concatable.Concatter;
import com.google.devtools.build.lib.util.SpellChecker;
import java.util.IllegalFormatException;
import java.util.List;
@@ -370,75 +369,285 @@
}
/** Evaluates an eager binary operation, {@code x op y}. (Excludes AND and OR.) */
- static Object binaryOp(TokenKind op, Object x, Object y, StarlarkThread thread, Location location)
+ static Object binaryOp(
+ TokenKind op, Object x, Object y, StarlarkSemantics semantics, Mutability mu)
throws EvalException {
- try {
- switch (op) {
- case PLUS:
- return plus(x, y, thread, location);
+ switch (op) {
+ case PLUS:
+ if (x instanceof Integer) {
+ if (y instanceof Integer) {
+ // int + int
+ int xi = (Integer) x;
+ int yi = (Integer) y;
+ int z = xi + yi;
+ // Overflow Detection, §2-13 Hacker's Delight:
+ // "operands have the same sign and the sum
+ // has a sign opposite to that of the operands."
+ if (((xi ^ z) & (yi ^ z)) < 0) {
+ throw Starlark.errorf("integer overflow in addition");
+ }
+ return z;
+ }
- case PIPE:
- return pipe(thread.getSemantics(), x, y);
+ } else if (x instanceof String) {
+ if (y instanceof String) {
+ // string + string
+ return (String) x + (String) y;
+ }
- case AMPERSAND:
- return and(x, y);
+ } else if (x instanceof Tuple) {
+ if (y instanceof Tuple) {
+ // tuple + tuple
+ return Tuple.concat((Tuple<?>) x, (Tuple<?>) y);
+ }
- case CARET:
- return xor(x, y);
+ } else if (x instanceof StarlarkList) {
+ if (y instanceof StarlarkList) {
+ // list + list
+ return StarlarkList.concat((StarlarkList<?>) x, (StarlarkList<?>) y, mu);
+ }
- case GREATER_GREATER:
- return rightShift(x, y);
+ } else if (x instanceof Depset) {
+ // depset + any
+ // TODO(bazel-team): Remove deprecated operator.
+ if (semantics.incompatibleDepsetUnion()) {
+ throw Starlark.errorf(
+ "`+` 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 Depset.unionOf((Depset) x, y);
+ }
+ break;
- case LESS_LESS:
- return leftShift(x, y);
+ case PIPE:
+ if (x instanceof Integer) {
+ if (y instanceof Integer) {
+ // int | int
+ return ((Integer) x) | (Integer) y;
+ }
+ } else if (x instanceof Depset) {
+ // depset | any
+ if (semantics.incompatibleDepsetUnion()) {
+ throw Starlark.errorf(
+ "`|` 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 Depset.unionOf((Depset) x, y);
+ }
+ break;
- case MINUS:
- return minus(x, y);
+ case AMPERSAND:
+ if (x instanceof Integer && y instanceof Integer) {
+ // int & int
+ return (Integer) x & (Integer) y;
+ }
+ break;
- case STAR:
- return star(thread.mutability(), x, y);
+ case CARET:
+ if (x instanceof Integer && y instanceof Integer) {
+ // int ^ int
+ return (Integer) x ^ (Integer) y;
+ }
+ break;
- case SLASH:
- throw Starlark.errorf(
- "The `/` operator is not allowed. Please use the `//` operator for integer"
- + " division.");
+ case GREATER_GREATER:
+ if (x instanceof Integer && y instanceof Integer) {
+ // int >> int
+ int xi = (Integer) x;
+ int yi = (Integer) y;
+ if (yi < 0) {
+ throw Starlark.errorf("negative shift count: %d", yi);
+ } else if (yi >= Integer.SIZE) {
+ return xi < 0 ? -1 : 0;
+ }
+ return xi >> yi;
+ }
+ break;
- case SLASH_SLASH:
- return slash(x, y);
+ case LESS_LESS:
+ if (x instanceof Integer && y instanceof Integer) {
+ // int << int
+ int xi = (Integer) x;
+ int yi = (Integer) y;
+ if (yi < 0) {
+ throw Starlark.errorf("negative shift count: %d", yi);
+ }
+ int z = xi << yi;
+ if (z >> yi != xi) {
+ throw Starlark.errorf("integer overflow");
+ }
+ return z;
+ }
+ break;
- case PERCENT:
- return percent(x, y);
+ case MINUS:
+ if (x instanceof Integer && y instanceof Integer) {
+ // int - int
+ int xi = (Integer) x;
+ int yi = (Integer) y;
+ int z = xi - yi;
+ if (((xi ^ yi) & (xi ^ z)) < 0) {
+ throw Starlark.errorf("integer overflow in subtraction");
+ }
+ return z;
+ }
+ break;
- case EQUALS_EQUALS:
- return x.equals(y);
+ case STAR:
+ if (x instanceof Integer) {
+ int xi = (Integer) x;
+ if (y instanceof Integer) {
+ // int * int
+ long z = (long) xi * (long) (Integer) y;
+ if ((int) z != z) {
+ throw Starlark.errorf("integer overflow in multiplication");
+ }
+ return (int) z;
+ } else if (y instanceof String) {
+ // int * string
+ return repeatString((String) y, xi);
+ } else if (y instanceof Tuple) {
+ // int * tuple
+ return ((Tuple<?>) y).repeat(xi);
+ } else if (y instanceof StarlarkList) {
+ // int * list
+ return ((StarlarkList<?>) y).repeat(xi, mu);
+ }
- case NOT_EQUALS:
- return !x.equals(y);
+ } else if (x instanceof String) {
+ if (y instanceof Integer) {
+ // string * int
+ return repeatString((String) x, (Integer) y);
+ }
- case LESS:
- return compare(x, y) < 0;
+ } else if (x instanceof Tuple) {
+ if (y instanceof Integer) {
+ // tuple * int
+ return ((Tuple<?>) x).repeat((Integer) y);
+ }
- case LESS_EQUALS:
- return compare(x, y) <= 0;
+ } else if (x instanceof StarlarkList) {
+ if (y instanceof Integer) {
+ // list * int
+ return ((StarlarkList<?>) x).repeat((Integer) y, mu);
+ }
+ }
+ break;
- case GREATER:
- return compare(x, y) > 0;
+ case SLASH:
+ throw Starlark.errorf("The `/` operator is not allowed. For integer division, use `//`.");
- case GREATER_EQUALS:
- return compare(x, y) >= 0;
+ case SLASH_SLASH:
+ if (x instanceof Integer && y instanceof Integer) {
+ // int // int
+ int xi = (Integer) x;
+ int yi = (Integer) y;
+ if (yi == 0) {
+ throw Starlark.errorf("integer division by zero");
+ }
+ // http://python-history.blogspot.com/2010/08/why-pythons-integer-division-floors.html
+ int quo = xi / yi;
+ int rem = xi % yi;
+ if ((xi < 0) != (yi < 0) && rem != 0) {
+ quo--;
+ }
+ return quo;
+ }
+ break;
- case IN:
- return in(thread.getSemantics(), x, y);
+ case PERCENT:
+ if (x instanceof Integer) {
+ if (y instanceof Integer) {
+ // int % int
+ int xi = (Integer) x;
+ int yi = (Integer) y;
+ if (yi == 0) {
+ throw Starlark.errorf("integer modulo by zero");
+ }
+ // In Starlark, the sign of the result is the sign of the divisor.
+ int z = xi % yi;
+ if ((xi < 0) != (yi < 0) && z != 0) {
+ z += yi;
+ }
+ return z;
+ }
- case NOT_IN:
- return !in(thread.getSemantics(), x, y);
+ } else if (x instanceof String) {
+ // string % any
+ String xs = (String) x;
+ try {
+ if (y instanceof Tuple) {
+ return Starlark.formatWithList(xs, (Tuple) y);
+ } else {
+ return Starlark.format(xs, y);
+ }
+ } catch (IllegalFormatException ex) {
+ throw new EvalException(null, ex.getMessage());
+ }
+ }
+ break;
- default:
- throw new AssertionError("Unsupported binary operator: " + op);
- }
- } catch (ArithmeticException ex) {
- throw new EvalException(null, ex.getMessage());
+ case EQUALS_EQUALS:
+ return x.equals(y);
+
+ case NOT_EQUALS:
+ return !x.equals(y);
+
+ case LESS:
+ return compare(x, y) < 0;
+
+ case LESS_EQUALS:
+ return compare(x, y) <= 0;
+
+ case GREATER:
+ return compare(x, y) > 0;
+
+ case GREATER_EQUALS:
+ return compare(x, y) >= 0;
+
+ case IN:
+ if (y instanceof SkylarkQueryable) {
+ return ((SkylarkQueryable) y).containsKey(semantics, x);
+ } else if (y instanceof String) {
+ if (!(x instanceof String)) {
+ throw Starlark.errorf(
+ "'in <string>' requires string as left operand, not '%s'", Starlark.type(x));
+ }
+ return ((String) y).contains((String) x);
+ }
+ break;
+
+ case NOT_IN:
+ Object z = binaryOp(TokenKind.IN, x, y, semantics, mu);
+ if (z != null) {
+ return !Starlark.truth(z);
+ }
+ break;
+
+ default:
+ throw new AssertionError("not a binary operator: " + op);
}
+
+ // custom binary operator?
+ if (x instanceof HasBinary) {
+ Object z = ((HasBinary) x).binaryOp(op, y, true);
+ if (z != null) {
+ return z;
+ }
+ }
+ if (y instanceof HasBinary) {
+ Object z = ((HasBinary) y).binaryOp(op, x, false);
+ if (z != null) {
+ return z;
+ }
+ }
+
+ throw Starlark.errorf(
+ "unsupported binary operation: %s %s %s", Starlark.type(x), op, Starlark.type(y));
}
/** Implements comparison operators. */
@@ -450,232 +659,8 @@
}
}
- /** Implements 'x + y'. */
- // StarlarkThread is needed for Mutability and Semantics.
- // Location is exposed to Selector{List,Value} and Concatter.
- static Object plus(Object x, Object y, StarlarkThread thread, Location location)
- 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 StarlarkList && y instanceof StarlarkList) {
- return StarlarkList.concat((StarlarkList<?>) x, (StarlarkList<?>) y, thread.mutability());
- }
-
- 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 unknownBinaryOperator(x, y, TokenKind.PLUS);
- }
- }
-
- // TODO(bazel-team): Remove deprecated operator.
- if (x instanceof Depset) {
- if (thread.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 Depset.unionOf((Depset) x, y);
- }
- throw unknownBinaryOperator(x, y, TokenKind.PLUS);
- }
-
- /** Implements 'x | y'. */
- private static Object pipe(StarlarkSemantics semantics, Object x, Object y) throws EvalException {
- if (x instanceof Integer && y instanceof Integer) {
- return ((Integer) x) | ((Integer) y);
- } else if (x instanceof Depset) {
- if (semantics.incompatibleDepsetUnion()) {
- throw Starlark.errorf(
- "`|` 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 Depset.unionOf((Depset) x, y);
- }
- throw unknownBinaryOperator(x, y, TokenKind.PIPE);
- }
-
- /** Implements 'x - y'. */
- private static Object minus(Object x, Object y) throws EvalException {
- if (x instanceof Integer && y instanceof Integer) {
- return Math.subtractExact((Integer) x, (Integer) y);
- }
- throw unknownBinaryOperator(x, y, TokenKind.MINUS);
- }
-
- /** Implements 'x * y'. */
- private static Object star(Mutability mu, Object x, Object y) 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) {
- return Strings.repeat((String) otherFactor, Math.max(0, number));
- } else if (otherFactor instanceof Tuple) {
- return ((Tuple<?>) otherFactor).repeat(number, mu);
- } else if (otherFactor instanceof StarlarkList) {
- return ((StarlarkList<?>) otherFactor).repeat(number, mu);
- }
- }
- throw unknownBinaryOperator(x, y, TokenKind.STAR);
- }
-
- /** Implements 'x // y'. */
- private static Object slash(Object x, Object y) throws EvalException {
- // int / int
- if (x instanceof Integer && y instanceof Integer) {
- if (y.equals(0)) {
- throw Starlark.errorf("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 unknownBinaryOperator(x, y, TokenKind.SLASH_SLASH);
- }
-
- /** Implements 'x % y'. */
- private static Object percent(Object x, Object y) throws EvalException {
- // int % int
- if (x instanceof Integer && y instanceof Integer) {
- if (y.equals(0)) {
- throw Starlark.errorf("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 Starlark.formatWithList(pattern, (Tuple) y);
- }
- return Starlark.format(pattern, y);
- } catch (IllegalFormatException ex) {
- throw new EvalException(null, ex.getMessage());
- }
- }
- throw unknownBinaryOperator(x, y, TokenKind.PERCENT);
- }
-
- /** Implements 'x & y'. */
- private static Object and(Object x, Object y) throws EvalException {
- if (x instanceof Integer && y instanceof Integer) {
- return (Integer) x & (Integer) y;
- }
- throw unknownBinaryOperator(x, y, TokenKind.AMPERSAND);
- }
-
- /** Implements 'x ^ y'. */
- private static Object xor(Object x, Object y) throws EvalException {
- if (x instanceof Integer && y instanceof Integer) {
- return (Integer) x ^ (Integer) y;
- }
- throw unknownBinaryOperator(x, y, TokenKind.CARET);
- }
-
- /** Implements 'x >> y'. */
- private static Object rightShift(Object x, Object y) throws EvalException {
- if (x instanceof Integer && y instanceof Integer) {
- if ((Integer) y < 0) {
- throw Starlark.errorf("negative shift count: %s", y);
- } else if ((Integer) y >= Integer.SIZE) {
- return ((Integer) x < 0) ? -1 : 0;
- }
- return (Integer) x >> (Integer) y;
- }
- throw unknownBinaryOperator(x, y, TokenKind.GREATER_GREATER);
- }
-
- /** Implements 'x << y'. */
- private static Object leftShift(Object x, Object y) throws EvalException {
- if (x instanceof Integer && y instanceof Integer) {
- if ((Integer) y < 0) {
- throw Starlark.errorf("negative shift count: %s", y);
- }
- Integer result = (Integer) x << (Integer) y;
- if (!rightShift(result, y).equals(x)) {
- throw new ArithmeticException("integer overflow");
- }
- return result;
- }
- throw unknownBinaryOperator(x, y, TokenKind.LESS_LESS);
- }
-
- /** Implements 'x in y'. */
- private static boolean in(StarlarkSemantics semantics, Object x, Object y) throws EvalException {
- if (y instanceof SkylarkQueryable) {
- return ((SkylarkQueryable) y).containsKey(semantics, x);
- } else if (y instanceof String) {
- if (x instanceof String) {
- return ((String) y).contains((String) x);
- } else {
- throw Starlark.errorf(
- "'in <string>' requires string as left operand, not '%s'", Starlark.type(x));
- }
- } else {
- throw Starlark.errorf(
- "unsupported binary operation: %s in %s (right operand must be string, sequence, or"
- + " dict)",
- Starlark.type(x), Starlark.type(y));
- }
- }
-
- /** Returns an exception signifying incorrect types for the given operator. */
- private static EvalException unknownBinaryOperator(Object x, Object y, TokenKind op) {
- return Starlark.errorf(
- "unsupported binary operation: %s %s %s", Starlark.type(x), op, Starlark.type(y));
+ private static String repeatString(String s, int n) {
+ return n <= 0 ? "" : Strings.repeat(s, n);
}
/** Evaluates a unary operation. */
@@ -686,12 +671,11 @@
case MINUS:
if (x instanceof Integer) {
- try {
- return Math.negateExact((Integer) x);
- } catch (ArithmeticException e) {
- // Fails for -MIN_INT.
- throw new EvalException(null, e.getMessage());
+ int xi = (Integer) x;
+ if (xi == Integer.MIN_VALUE) {
+ throw Starlark.errorf("integer overflow in negation");
}
+ return -xi;
}
break;
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/HasBinary.java b/src/main/java/com/google/devtools/build/lib/syntax/HasBinary.java
new file mode 100644
index 0000000..d5b5566
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/syntax/HasBinary.java
@@ -0,0 +1,40 @@
+// Copyright 2020 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 javax.annotation.Nullable;
+
+/**
+ * A Starlark value that supports binary operators such as {@code x+y}.
+ *
+ * <p>During evaluation of a Starlark binary operation, if none of the built-in cases match, then
+ * the left operand is queried; if it implements HasBinary, its {@link #binaryOp} method is called.
+ * If the left operand does not implement HasBinary, or declines to implement the particular
+ * operation by returning null, then the right operand is queried for HasBinary and its {@link
+ * #binaryOp} method is called. If neither operand defines the operator, evaluation fails.
+ *
+ * <p>Subclasses should strive for appropriate symmetries in their implementations, such as {@code x
+ * * y == y * x}.
+ */
+// TODO(adonovan): rename BinaryOperand?
+public interface HasBinary extends StarlarkValue {
+
+ /**
+ * Returns {@code this op that}, if thisLeft, or {@code that op this} otherwise. May return null
+ * to indicate that the operation is not supported, or may throw a specific exception.
+ */
+ @Nullable
+ Object binaryOp(TokenKind op, Object that, boolean thisLeft) throws EvalException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SelectorList.java b/src/main/java/com/google/devtools/build/lib/syntax/SelectorList.java
index 52d464c..57b57a7 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SelectorList.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SelectorList.java
@@ -16,9 +16,9 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
-import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -44,10 +44,11 @@
doc = "A selector between configuration-dependent entities.",
documented = false)
@AutoCodec
-public final class SelectorList implements StarlarkValue {
- // TODO(build-team): Selectors are currently split between .packages and .syntax . They should
- // really all be in .packages, but then we'd need to figure out a way how to extend binary
- // operators, which is a non-trivial problem.
+public final class SelectorList implements StarlarkValue, HasBinary {
+
+ // TODO(adonovan): move to lib.packages.
+ // TODO(adonovan): combine Selector{List,Value}. There's no need for two data types.
+
private final Class<?> type;
private final List<Object> elements;
@@ -80,14 +81,21 @@
}
/**
- * Creates a list that concatenates two values, where each value may be a native
- * type, a select over that type, or a selector list over that type.
+ * Creates a list that concatenates two values, where each value may be a native type, a select
+ * over that type, or a selector list over that type.
*
* @throws EvalException if the values don't have the same underlying type
*/
- public static SelectorList concat(Location location, Object value1, Object value2)
- throws EvalException {
- return of(location, value1, value2);
+ public static SelectorList concat(Object x, Object y) throws EvalException {
+ return of(Arrays.asList(x, y));
+ }
+
+ @Override
+ public SelectorList binaryOp(TokenKind op, Object that, boolean thisLeft) throws EvalException {
+ if (op == TokenKind.PLUS) {
+ return thisLeft ? concat(this, that) : concat(that, this);
+ }
+ return null;
}
/**
@@ -96,17 +104,7 @@
*
* @throws EvalException if all values don't have the same underlying type
*/
- public static SelectorList of(Location location, Object... values) throws EvalException {
- return SelectorList.of(location, ImmutableList.copyOf(values));
- }
-
- /**
- * Creates a list from the given sequence of values, which must be non-empty. Each value may be a
- * native type, a select over that type, or a selector list over that type.
- *
- * @throws EvalException if all values don't have the same underlying type
- */
- public static SelectorList of(Location location, Iterable<?> values) throws EvalException {
+ public static SelectorList of(Iterable<?> values) throws EvalException {
Preconditions.checkArgument(!Iterables.isEmpty(values));
ImmutableList.Builder<Object> elements = ImmutableList.builder();
Object firstValue = null;
@@ -121,11 +119,9 @@
firstValue = value;
}
if (!canConcatenate(getNativeType(firstValue), getNativeType(value))) {
- throw new EvalException(
- location,
- String.format(
- "'+' operator applied to incompatible types (%s, %s)",
- getTypeName(firstValue), getTypeName(value)));
+ throw Starlark.errorf(
+ "'+' operator applied to incompatible types (%s, %s)",
+ getTypeName(firstValue), getTypeName(value));
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SelectorValue.java b/src/main/java/com/google/devtools/build/lib/syntax/SelectorValue.java
index 97c5005..ba6359c 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SelectorValue.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SelectorValue.java
@@ -37,10 +37,11 @@
doc = "A selector between configuration-dependent entities.",
documented = false)
@AutoCodec
-public final class SelectorValue implements StarlarkValue {
- // TODO(bazel-team): Selectors are currently split between .packages and .syntax . They should
- // really all be in .packages, but then we'd need to figure out a way how to extend binary
- // operators, which is a non-trivial problem.
+public final class SelectorValue implements StarlarkValue, HasBinary {
+
+ // TODO(adonovan): move to lib.packages.
+ // TODO(adonovan): combine Selector{List,Value}. There's no need for two data types.
+
private final ImmutableMap<?, ?> dictionary;
private final Class<?> type;
private final String noMatchError;
@@ -74,6 +75,14 @@
}
@Override
+ public SelectorList binaryOp(TokenKind op, Object that, boolean thisLeft) throws EvalException {
+ if (op == TokenKind.PLUS) {
+ return thisLeft ? SelectorList.concat(this, that) : SelectorList.concat(that, this);
+ }
+ return null;
+ }
+
+ @Override
public void repr(Printer printer) {
printer.format("select(%r)", dictionary);
}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Tuple.java b/src/main/java/com/google/devtools/build/lib/syntax/Tuple.java
index aa8dd19..d502e7d 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Tuple.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Tuple.java
@@ -221,7 +221,7 @@
}
/** Returns a Tuple containing n consecutive repeats of this tuple. */
- public Tuple<E> repeat(int n, Mutability mutability) {
+ Tuple<E> repeat(int n) {
if (n <= 0 || isEmpty()) {
return empty();
}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/BuildTypeTest.java b/src/test/java/com/google/devtools/build/lib/packages/BuildTypeTest.java
index b18a701..9db91b9 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/BuildTypeTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/BuildTypeTest.java
@@ -23,7 +23,6 @@
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.RepositoryName;
-import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.packages.BuildType.LabelConversionContext;
import com.google.devtools.build.lib.packages.BuildType.Selector;
import com.google.devtools.build.lib.packages.Type.ConversionException;
@@ -461,7 +460,7 @@
List<String> list = ImmutableList.of("//a:a", "//b:b");
// Creating a SelectorList from a SelectorList and a list should work properly.
- SelectorList result = SelectorList.of(Location.BUILTIN, selectorList, list);
+ SelectorList result = SelectorList.concat(selectorList, list);
assertThat(result).isNotNull();
assertThat(result.getType()).isAssignableTo(List.class);
}
@@ -473,7 +472,7 @@
List<String> list = ImmutableList.of("//a:a", "//b:b");
// Creating a SelectorList from a SelectorValue and a list should work properly.
- SelectorList result = SelectorList.of(Location.BUILTIN, selectorValue, list);
+ SelectorList result = SelectorList.concat(selectorValue, list);
assertThat(result).isNotNull();
assertThat(result.getType()).isAssignableTo(List.class);
}
@@ -485,7 +484,7 @@
arrayList.add("//a:a");
// Creating a SelectorList from two lists of different types should work properly.
- SelectorList result = SelectorList.of(Location.BUILTIN, list, arrayList);
+ SelectorList result = SelectorList.concat(list, arrayList);
assertThat(result).isNotNull();
assertThat(result.getType()).isAssignableTo(List.class);
}
@@ -495,7 +494,7 @@
List<String> list = ImmutableList.of("//a:a", "//b:b");
// Creating a SelectorList from a list and a non-list should fail.
- assertThrows(EvalException.class, () -> SelectorList.of(Location.BUILTIN, list, "A string"));
+ assertThrows(EvalException.class, () -> SelectorList.concat(list, "A string"));
}
/**
diff --git a/src/test/java/com/google/devtools/build/lib/packages/SkylarkInfoTest.java b/src/test/java/com/google/devtools/build/lib/packages/SkylarkInfoTest.java
index 2fa6234..b5ab54f 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/SkylarkInfoTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/SkylarkInfoTest.java
@@ -22,6 +22,7 @@
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.StarlarkValue;
+import com.google.devtools.build.lib.syntax.TokenKind;
import javax.annotation.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -84,8 +85,7 @@
SkylarkInfo info1 = makeInfoWithF1F2Values(provider1, 4, 5);
SkylarkInfo info2 = makeInfoWithF1F2Values(provider2, 4, 5);
EvalException expected =
- assertThrows(
- EvalException.class, () -> info1.getConcatter().concat(info1, info2, Location.BUILTIN));
+ assertThrows(EvalException.class, () -> info1.binaryOp(TokenKind.PLUS, info2, true));
assertThat(expected).hasMessageThat()
.contains("Cannot use '+' operator on instances of different providers");
}
@@ -96,8 +96,7 @@
SkylarkInfo info1 = makeInfoWithF1F2Values(provider1, 4, 5);
SkylarkInfo info2 = makeInfoWithF1F2Values(provider1, 4, null);
EvalException expected =
- assertThrows(
- EvalException.class, () -> info1.getConcatter().concat(info1, info2, Location.BUILTIN));
+ assertThrows(EvalException.class, () -> info1.binaryOp(TokenKind.PLUS, info2, true));
assertThat(expected)
.hasMessageThat()
.contains("cannot add struct instances with common field 'f1'");
@@ -108,7 +107,7 @@
SkylarkProvider provider = makeProvider();
SkylarkInfo info1 = makeInfoWithF1F2Values(provider, 4, null);
SkylarkInfo info2 = makeInfoWithF1F2Values(provider, null, 5);
- SkylarkInfo result = (SkylarkInfo) info1.getConcatter().concat(info1, info2, Location.BUILTIN);
+ SkylarkInfo result = info1.binaryOp(TokenKind.PLUS, info2, true);
assertThat(result.getFieldNames()).containsExactly("f1", "f2");
assertThat(result.getValue("f1")).isEqualTo(4);
assertThat(result.getValue("f2")).isEqualTo(5);
@@ -119,7 +118,7 @@
SkylarkProvider provider = makeProvider();
SkylarkInfo info1 = makeInfoWithF1F2Values(provider, 4, null);
SkylarkInfo info2 = makeInfoWithF1F2Values(provider, null, 5);
- SkylarkInfo result = (SkylarkInfo) info1.getConcatter().concat(info1, info2, Location.BUILTIN);
+ SkylarkInfo result = info1.binaryOp(TokenKind.PLUS, info2, true);
assertThat(result.getFieldNames()).containsExactly("f1", "f2");
assertThat(result.getValue("f1")).isEqualTo(4);
assertThat(result.getValue("f2")).isEqualTo(5);
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
index 60a0d1d..c930845 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
@@ -737,8 +737,7 @@
newTest()
.testIfErrorContains(
"'in <string>' requires string as left operand, not 'int'", "1 in '123'")
- .testIfErrorContains(
- "unsupported binary operation: string in int (right operand must be", "'a' in 1");
+ .testIfErrorContains("unsupported binary operation: string in int", "'a' in 1");
}
@Test