Support bitwise operations

Add support for `~`, `&`, `|`, `^`, `<<`, `>>` bitwise operations.
Implements: https://github.com/bazelbuild/starlark/issues/20#issuecomment-456647994

Closes #8903.

PiperOrigin-RevId: 259732302
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java
index 8ff947b..d2b334e 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java
@@ -189,6 +189,18 @@
         case PIPE:
           return pipe(x, y, env, location);
 
+        case AMPERSAND:
+          return and(x, y, location);
+
+        case CARET:
+          return xor(x, y, location);
+
+        case GREATER_GREATER:
+          return rightShift(x, y, location);
+
+        case LESS_LESS:
+          return leftShift(x, y, location);
+
         case MINUS:
           return minus(x, y, location);
 
@@ -346,7 +358,9 @@
   /** Implements 'x | y'. */
   private static Object pipe(Object x, Object y, Environment env, Location location)
       throws EvalException {
-    if (x instanceof SkylarkNestedSet) {
+    if (x instanceof Integer && y instanceof Integer) {
+      return ((Integer) x) | ((Integer) y);
+    } else if (x instanceof SkylarkNestedSet) {
       if (env.getSemantics().incompatibleDepsetUnion()) {
         throw new EvalException(
             location,
@@ -447,6 +461,50 @@
     throw typeException(x, y, TokenKind.PERCENT, location);
   }
 
+  /** Implements 'x & y'. */
+  private static Object and(Object x, Object y, Location location) throws EvalException {
+    if (x instanceof Integer && y instanceof Integer) {
+      return (Integer) x & (Integer) y;
+    }
+    throw typeException(x, y, TokenKind.AMPERSAND, location);
+  }
+
+  /** Implements 'x ^ y'. */
+  private static Object xor(Object x, Object y, Location location) throws EvalException {
+    if (x instanceof Integer && y instanceof Integer) {
+      return (Integer) x ^ (Integer) y;
+    }
+    throw typeException(x, y, TokenKind.CARET, location);
+  }
+
+  /** Implements 'x >> y'. */
+  private static Object rightShift(Object x, Object y, Location location) throws EvalException {
+    if (x instanceof Integer && y instanceof Integer) {
+      if ((Integer) y < 0) {
+        throw new EvalException(location, "negative shift count: " + y);
+      } else if ((Integer) y >= Integer.SIZE) {
+        return ((Integer) x < 0) ? -1 : 0;
+      }
+      return (Integer) x >> (Integer) y;
+    }
+    throw typeException(x, y, TokenKind.GREATER_GREATER, location);
+  }
+
+  /** Implements 'x << y'. */
+  private static Object leftShift(Object x, Object y, Location location) throws EvalException {
+    if (x instanceof Integer && y instanceof Integer) {
+      if ((Integer) y < 0) {
+        throw new EvalException(location, "negative shift count: " + y);
+      }
+      Integer result = (Integer) x << (Integer) y;
+      if (!rightShift(result, y, location).equals(x)) {
+        throw new ArithmeticException("integer overflow");
+      }
+      return result;
+    }
+    throw typeException(x, y, TokenKind.LESS_LESS, 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,
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Lexer.java b/src/main/java/com/google/devtools/build/lib/syntax/Lexer.java
index c7f424b..fd11e55 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Lexer.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Lexer.java
@@ -52,6 +52,9 @@
           .put('*', TokenKind.STAR_EQUALS)
           .put('/', TokenKind.SLASH_EQUALS)
           .put('%', TokenKind.PERCENT_EQUALS)
+          .put('^', TokenKind.CARET_EQUALS)
+          .put('&', TokenKind.AMPERSAND_EQUALS)
+          .put('|', TokenKind.PIPE_EQUALS)
           .build();
 
   private final EventHandler eventHandler;
@@ -809,10 +812,26 @@
           popParen();
           break;
         case '>':
-          setToken(TokenKind.GREATER, pos - 1, pos);
+          if (lookaheadIs(0, '>') && lookaheadIs(1, '=')) {
+            setToken(TokenKind.GREATER_GREATER_EQUALS, pos - 1, pos + 2);
+            pos += 2;
+          } else if (lookaheadIs(0, '>')) {
+            setToken(TokenKind.GREATER_GREATER, pos - 1, pos + 1);
+            pos += 1;
+          } else {
+            setToken(TokenKind.GREATER, pos - 1, pos);
+          }
           break;
         case '<':
-          setToken(TokenKind.LESS, pos - 1, pos);
+          if (lookaheadIs(0, '<') && lookaheadIs(1, '=')) {
+            setToken(TokenKind.LESS_LESS_EQUALS, pos - 1, pos + 2);
+            pos += 2;
+          } else if (lookaheadIs(0, '<')) {
+            setToken(TokenKind.LESS_LESS, pos - 1, pos + 1);
+            pos += 1;
+          } else {
+            setToken(TokenKind.LESS, pos - 1, pos);
+          }
           break;
         case ':':
           setToken(TokenKind.COLON, pos - 1, pos);
@@ -835,6 +854,15 @@
         case '%':
           setToken(TokenKind.PERCENT, pos - 1, pos);
           break;
+        case '~':
+          setToken(TokenKind.TILDE, pos - 1, pos);
+          break;
+        case '&':
+          setToken(TokenKind.AMPERSAND, pos - 1, pos);
+          break;
+        case '^':
+          setToken(TokenKind.CARET, pos - 1, pos);
+          break;
         case '/':
           if (lookaheadIs(0, '/') && lookaheadIs(1, '=')) {
             setToken(TokenKind.SLASH_SLASH_EQUALS, pos - 1, pos + 2);
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Parser.java b/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
index fa25198..f1912e9 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Parser.java
@@ -134,6 +134,11 @@
           .put(TokenKind.SLASH_EQUALS, TokenKind.SLASH)
           .put(TokenKind.SLASH_SLASH_EQUALS, TokenKind.SLASH_SLASH)
           .put(TokenKind.PERCENT_EQUALS, TokenKind.PERCENT)
+          .put(TokenKind.AMPERSAND_EQUALS, TokenKind.AMPERSAND)
+          .put(TokenKind.CARET_EQUALS, TokenKind.CARET)
+          .put(TokenKind.PIPE_EQUALS, TokenKind.PIPE)
+          .put(TokenKind.GREATER_GREATER_EQUALS, TokenKind.GREATER_GREATER)
+          .put(TokenKind.LESS_LESS_EQUALS, TokenKind.LESS_LESS)
           .build();
 
   /**
@@ -155,6 +160,9 @@
               TokenKind.IN,
               TokenKind.NOT_IN),
           EnumSet.of(TokenKind.PIPE),
+          EnumSet.of(TokenKind.CARET),
+          EnumSet.of(TokenKind.AMPERSAND),
+          EnumSet.of(TokenKind.GREATER_GREATER, TokenKind.LESS_LESS),
           EnumSet.of(TokenKind.MINUS, TokenKind.PLUS),
           EnumSet.of(TokenKind.SLASH, TokenKind.SLASH_SLASH, TokenKind.STAR, TokenKind.PERCENT));
 
@@ -680,6 +688,13 @@
           UnaryOperatorExpression plus = new UnaryOperatorExpression(TokenKind.PLUS, expr);
           return setLocation(plus, start, expr);
         }
+      case TILDE:
+        {
+          nextToken();
+          Expression expr = parsePrimaryWithSuffix();
+          UnaryOperatorExpression tilde = new UnaryOperatorExpression(TokenKind.TILDE, expr);
+          return setLocation(tilde, start, expr);
+        }
       default:
         {
           syntaxError("expected expression");
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/TokenKind.java b/src/main/java/com/google/devtools/build/lib/syntax/TokenKind.java
index b85743c..aca3523 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/TokenKind.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/TokenKind.java
@@ -18,11 +18,14 @@
  * A TokenKind is an enumeration of each different kind of lexical symbol.
  */
 public enum TokenKind {
-
+  AMPERSAND("&"),
+  AMPERSAND_EQUALS("&="),
   AND("and"),
   AS("as"),
   ASSERT("assert"),
   BREAK("break"),
+  CARET("^"),
+  CARET_EQUALS("^="),
   CLASS("class"),
   COLON(":"),
   COMMA(","),
@@ -42,6 +45,8 @@
   GLOBAL("global"),
   GREATER(">"),
   GREATER_EQUALS(">="),
+  GREATER_GREATER(">>"),
+  GREATER_GREATER_EQUALS(">>="),
   IDENTIFIER("identifier"),
   IF("if"),
   ILLEGAL("illegal character"),
@@ -55,6 +60,8 @@
   LBRACKET("["),
   LESS("<"),
   LESS_EQUALS("<="),
+  LESS_LESS("<<"),
+  LESS_LESS_EQUALS("<<="),
   LOAD("load"),
   LPAREN("("),
   MINUS("-"),
@@ -70,6 +77,7 @@
   PERCENT("%"),
   PERCENT_EQUALS("%="),
   PIPE("|"),
+  PIPE_EQUALS("|="),
   PLUS("+"),
   PLUS_EQUALS("+="),
   RAISE("raise"),
@@ -86,6 +94,7 @@
   STAR_EQUALS("*="),
   STAR_STAR("**"),
   STRING("string literal"),
+  TILDE("~"),
   TRY("try"),
   WHILE("while"),
   WITH("with"),
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/UnaryOperatorExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/UnaryOperatorExpression.java
index b16a07f..be44002 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/UnaryOperatorExpression.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/UnaryOperatorExpression.java
@@ -19,7 +19,7 @@
 /** A UnaryOperatorExpression represents a unary operator expression, 'op x'. */
 public final class UnaryOperatorExpression extends Expression {
 
-  private final TokenKind op; // NOT, MINUS or PLUS
+  private final TokenKind op; // NOT, TILDE, MINUS or PLUS
   private final Expression x;
 
   public UnaryOperatorExpression(TokenKind op, Expression x) {
@@ -63,30 +63,34 @@
         return !EvalUtils.toBoolean(value);
 
       case MINUS:
-        if (!(value instanceof Integer)) {
-          throw new EvalException(
-              loc,
-              String.format(
-                  "unsupported operand type for -: '%s'", EvalUtils.getDataTypeName(value)));
+        if (value instanceof Integer) {
+          try {
+            return Math.negateExact((Integer) value);
+          } catch (ArithmeticException e) {
+            // Fails for -MIN_INT.
+            throw new EvalException(loc, e.getMessage());
+          }
         }
-        try {
-          return Math.negateExact((Integer) value);
-        } catch (ArithmeticException e) {
-          // Fails for -MIN_INT.
-          throw new EvalException(loc, e.getMessage());
-        }
-      case PLUS:
-        if (!(value instanceof Integer)) {
-          throw new EvalException(
-              loc,
-              String.format(
-                  "unsupported operand type for +: '%s'", EvalUtils.getDataTypeName(value)));
-        }
-        return value;
+        break;
 
+      case PLUS:
+        if (value instanceof Integer) {
+          return value;
+        }
+        break;
+
+      case TILDE:
+        if (value instanceof Integer) {
+          return ~((Integer) value);
+        }
+        break;
+
+        // ignore any other operator and proceed to report an error
       default:
-        throw new AssertionError("Unsupported unary operator: " + op);
     }
+    throw new EvalException(
+        loc,
+        String.format("unsupported unary operation: %s%s", op, EvalUtils.getDataTypeName(value)));
   }
 
   @Override
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 d199a91..544a583 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
@@ -62,7 +62,7 @@
         .testStatement("8 % 3", 2)
         .testIfErrorContains("unsupported operand type(s) for %: 'int' and 'string'", "3 % 'foo'")
         .testStatement("-5", -5)
-        .testIfErrorContains("unsupported operand type for -: 'string'", "-'foo'");
+        .testIfErrorContains("unsupported unary operation: -string", "-'foo'");
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java b/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
index bc4d8cc..2e14cb8 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
@@ -473,6 +473,10 @@
     assertThat(parseFile("x *= 1").toString()).isEqualTo("[x *= 1\n]");
     assertThat(parseFile("x /= 1").toString()).isEqualTo("[x /= 1\n]");
     assertThat(parseFile("x %= 1").toString()).isEqualTo("[x %= 1\n]");
+    assertThat(parseFile("x |= 1").toString()).isEqualTo("[x |= 1\n]");
+    assertThat(parseFile("x &= 1").toString()).isEqualTo("[x &= 1\n]");
+    assertThat(parseFile("x <<= 1").toString()).isEqualTo("[x <<= 1\n]");
+    assertThat(parseFile("x >>= 1").toString()).isEqualTo("[x >>= 1\n]");
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
index 87ba491..a0f9cab 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
@@ -1504,7 +1504,7 @@
     new SkylarkTest("--incompatible_depset_union=false")
         .testStatement("str(depset([1, 3]) | depset([1, 2]))", "depset([1, 2, 3])")
         .testStatement("str(depset([1, 2]) | [1, 3])", "depset([1, 2, 3])")
-        .testIfExactError("unsupported operand type(s) for |: 'int' and 'int'", "2 | 4");
+        .testIfExactError("unsupported operand type(s) for |: 'int' and 'bool'", "2 | False");
   }
 
   @Test
diff --git a/src/test/starlark/testdata/int.sky b/src/test/starlark/testdata/int.sky
index 91212da..d4710cf 100644
--- a/src/test/starlark/testdata/int.sky
+++ b/src/test/starlark/testdata/int.sky
@@ -73,3 +73,66 @@
 1 // 0  ### integer division by zero
 ---
 1 % 0  ### integer modulo by zero
+---
+
+# bitwise
+
+def f():
+  x = 2
+  x &= 1
+  assert_eq(x, 0)
+  x = 0
+  x |= 2
+  assert_eq(x, 2)
+  x ^= 3
+  assert_eq(x, 1)
+  x <<= 2
+  assert_eq(x, 4)
+  x >>=2
+  assert_eq(x, 1)
+
+f()
+---
+assert_eq(1 | 2, 3)
+assert_eq(3 | 6, 7)
+assert_eq(7 | 0, 7)
+assert_eq(7 & 0, 0)
+assert_eq(7 & 7, 7)
+assert_eq(7 & 2, 2)
+assert_eq((1|2) & (2|4), 2)
+assert_eq(1 ^ 2, 3)
+assert_eq(2 ^ 2, 0)
+assert_eq(-6 ^ 0, -6)
+assert_eq(1 | 0 ^ 1, 1) # check | and ^ operators precedence
+assert_eq(~1, -2)
+assert_eq(~-2, 1)
+assert_eq(~0, -1)
+assert_eq(~6, -7)
+assert_eq(~0, -1)
+assert_eq(~2147483647, -2147483647 - 1);
+assert_eq(1 << 2, 4)
+assert_eq(7 << 0, 7)
+assert_eq(-1 << 31, -2147483647 - 1)
+assert_eq(2 >> 1, 1)
+assert_eq(7 >> 0, 7)
+assert_eq(0 >> 0, 0)
+assert_eq(1000 >> 100, 0)
+assert_eq(-10 >> 1000, -1)
+
+# precedence
+
+assert_eq(8 | 3 ^ 4 & -2, 15)
+assert_eq(~8 >> 1 | 3 ^ 4 & -2 << 2 * 3 + 4 // -2, -5)
+
+---
+1 & False ### unsupported operand type(s) for &: 'int' and 'bool'
+---
+"a" ^ 5 ### unsupported operand type(s) for ^: 'string' and 'int'
+---
+~False ### unsupported unary operation: ~bool
+---
+1 << 31 ### integer overflow
+---
+1 << -4 ### negative shift count: -4
+---
+2 >> -1 ### negative shift count: -1