Skylark: int() can now declare a base parameter.

--
MOS_MIGRATED_REVID=126434299
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java b/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java
index ad52534..f02b585 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java
@@ -17,6 +17,7 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
@@ -1762,33 +1763,92 @@
     }
   };
 
-  @SkylarkSignature(name = "int", returnType = Integer.class, doc = "Converts a value to int. "
-      + "If the argument is a string, it is converted using base 10 and raises an error if the "
-      + "conversion fails. If the argument is a bool, it returns 0 (False) or 1 (True). "
-      + "If the argument is an int, it is simply returned."
-      + "<pre class=\"language-python\">int(\"123\") == 123</pre>",
-      parameters = {
-        @Param(name = "x", type = Object.class, doc = "The string to convert.")},
-      useLocation = true)
-  private static final BuiltinFunction int_ = new BuiltinFunction("int") {
-    public Integer invoke(Object x, Location loc) throws EvalException {
-      if (x instanceof Boolean) {
-        return ((Boolean) x).booleanValue() ? 1 : 0;
-      } else if (x instanceof Integer) {
-        return (Integer) x;
-      } else if (x instanceof String) {
-        try {
-          return Integer.parseInt((String) x);
-        } catch (NumberFormatException e) {
-          throw new EvalException(loc,
-              "invalid literal for int(): " + Printer.repr(x));
+  @SkylarkSignature(
+    name = "int",
+    returnType = Integer.class,
+    doc =
+        "Converts a value to int. "
+            + "If the argument is a string, it is converted using the given base and raises an "
+            + "error if the conversion fails. "
+            + "The base can be between 2 and 36 (inclusive) and defaults to 10. "
+            + "The value can be prefixed with 0b/0o/ox to represent values in base 2/8/16. "
+            + "If such a prefix is present, a base of 0 can be used to automatically determine the "
+            + "correct base: "
+            + "<pre class=\"language-python\">int(\"0xFF\", 0) == int(\"0xFF\", 16) == 255</pre>"
+            + "If the argument is a bool, it returns 0 (False) or 1 (True). "
+            + "If the argument is an int, it is simply returned."
+            + "<pre class=\"language-python\">int(\"123\") == 123</pre>",
+    parameters = {
+      @Param(name = "x", type = Object.class, doc = "The string to convert."),
+      @Param(
+        name = "base",
+        type = Integer.class,
+        defaultValue = "10",
+        doc = "The base of the string."
+      )
+    },
+    useLocation = true
+  )
+  private static final BuiltinFunction int_ =
+      new BuiltinFunction("int") {
+        private final ImmutableMap<String, Integer> intPrefixes =
+            ImmutableMap.of("0b", 2, "0o", 8, "0x", 16);
+
+        @SuppressWarnings("unused")
+        public Integer invoke(Object x, Integer base, Location loc) throws EvalException {
+          if (x instanceof String) {
+            return fromString(x, loc, base);
+          } else {
+            if (base != 10) {
+              throw new EvalException(loc, "int() can't convert non-string with explicit base");
+            }
+            if (x instanceof Boolean) {
+              return ((Boolean) x).booleanValue() ? 1 : 0;
+            } else if (x instanceof Integer) {
+              return (Integer) x;
+            }
+            throw new EvalException(
+                loc, Printer.format("%r is not of type string or int or bool", x));
+          }
         }
-      } else {
-        throw new EvalException(loc,
-            Printer.format("%r is not of type string or int or bool", x));
-      }
-    }
-  };
+
+        private int fromString(Object x, Location loc, int base) throws EvalException {
+          String value = (String) x;
+          String prefix = getIntegerPrefix(value);
+
+          if (!prefix.isEmpty()) {
+            value = value.substring(prefix.length());
+            int expectedBase = intPrefixes.get(prefix);
+            if (base == 0) {
+              // Similar to Python, base 0 means "derive the base from the prefix".
+              base = expectedBase;
+            } else if (base != expectedBase) {
+              throw new EvalException(
+                  loc, Printer.format("invalid literal for int() with base %d: %r", base, x));
+            }
+          }
+
+          if (base < 2 || base > 36) {
+            throw new EvalException(loc, "int() base must be >= 2 and <= 36");
+          }
+          try {
+            return Integer.parseInt(value, base);
+          } catch (NumberFormatException e) {
+            throw new EvalException(
+                loc, Printer.format("invalid literal for int() with base %d: %r", base, x));
+          }
+        }
+
+        private String getIntegerPrefix(String value) {
+          value = value.toLowerCase();
+          for (String prefix : intPrefixes.keySet()) {
+            if (value.startsWith(prefix)) {
+              return prefix;
+            }
+          }
+          return "";
+        }
+      };
 
   @SkylarkSignature(name = "struct", returnType = SkylarkClassObject.class, doc =
       "Creates an immutable struct using the keyword arguments as attributes. It is used to group "
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java b/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
index 279db38..a9f82d5 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
@@ -1658,8 +1658,8 @@
     new BothModesTest()
         .testStatement("int('1')", 1)
         .testStatement("int('-1234')", -1234)
-        .testIfErrorContains("invalid literal for int(): \"1.5\"", "int('1.5')")
-        .testIfErrorContains("invalid literal for int(): \"ab\"", "int('ab')")
+        .testIfErrorContains("invalid literal for int() with base 10: \"1.5\"", "int('1.5')")
+        .testIfErrorContains("invalid literal for int() with base 10: \"ab\"", "int('ab')")
         .testStatement("int(42)", 42)
         .testStatement("int(-1)", -1)
         .testStatement("int(True)", 1)
@@ -1668,6 +1668,45 @@
   }
 
   @Test
+  public void testIntWithBase() throws Exception {
+    new BothModesTest()
+        .testStatement("int('11', 2)", 3)
+        .testStatement("int('11', 9)", 10)
+        .testStatement("int('AF', 16)", 175)
+        .testStatement("int('11', 36)", 37)
+        .testStatement("int('az', 36)", 395);
+  }
+
+  @Test
+  public void testIntWithBase_InvalidBase() throws Exception {
+    new BothModesTest()
+        .testIfExactError("invalid literal for int() with base 3: \"123\"", "int('123', 3)")
+        .testIfExactError("invalid literal for int() with base 15: \"FF\"", "int('FF', 15)")
+        .testIfExactError("int() base must be >= 2 and <= 36", "int('123', -1)")
+        .testIfExactError("int() base must be >= 2 and <= 36", "int('123', 1)")
+        .testIfExactError("int() base must be >= 2 and <= 36", "int('123', 37)");
+  }
+
+  @Test
+  public void testIntWithBase_Prefix() throws Exception {
+    new BothModesTest()
+        .testStatement("int('0b11', 0)", 3)
+        .testStatement("int('0B11', 2)", 3)
+        .testStatement("int('0o11', 0)", 9)
+        .testStatement("int('0O11', 8)", 9)
+        .testStatement("int('0XFF', 0)", 255)
+        .testStatement("int('0xFF', 16)", 255)
+        .testIfExactError("invalid literal for int() with base 8: \"0xFF\"", "int('0xFF', 8)");
+  }
+
+  @Test
+  public void testIntWithBase_NoString() throws Exception {
+    new BothModesTest()
+        .testIfExactError("int() can't convert non-string with explicit base", "int(True, 2)")
+        .testIfExactError("int() can't convert non-string with explicit base", "int(1, 2)");
+  }
+
+  @Test
   public void testStrFunction() throws Exception {
     new SkylarkTest().testStatement("def foo(x): return x\nstr(foo)", "<function foo>");
   }