starlark: move MethodLibrary.parseInt to StarlarkInt

Also:
- simplify prefix logic, avoiding hash table.
- fix bug that allowed two signs ("-0x-10"), and test.
PiperOrigin-RevId: 339374232
diff --git a/src/main/java/net/starlark/java/eval/MethodLibrary.java b/src/main/java/net/starlark/java/eval/MethodLibrary.java
index cd24351..ebb91e0 100644
--- a/src/main/java/net/starlark/java/eval/MethodLibrary.java
+++ b/src/main/java/net/starlark/java/eval/MethodLibrary.java
@@ -15,17 +15,14 @@
 package net.starlark.java.eval;
 
 import com.google.common.base.Ascii;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
-import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.NoSuchElementException;
-import javax.annotation.Nullable;
 import net.starlark.java.annot.Param;
 import net.starlark.java.annot.ParamType;
 import net.starlark.java.annot.StarlarkBuiltin;
@@ -396,9 +393,6 @@
     }
   }
 
-  private static final ImmutableMap<String, Integer> INT_PREFIXES =
-      ImmutableMap.of("0b", 2, "0o", 8, "0x", 16);
-
   @StarlarkMethod(
       name = "int",
       doc =
@@ -480,80 +474,6 @@
     throw Starlark.errorf("got %s, want string, int, float, or bool", Starlark.type(x));
   }
 
-  // TODO(adonovan): move into StarlarkInt.parse in a follow-up.
-  static StarlarkInt parseInt(String string, int base) throws NumberFormatException {
-    String stringForErrors = string;
-
-    boolean isNegative = false;
-    if (string.isEmpty()) {
-      throw new NumberFormatException("empty string");
-    }
-    char c = string.charAt(0);
-    if (c == '+') {
-      string = string.substring(1);
-    } else if (c == '-') {
-      string = string.substring(1);
-      isNegative = true;
-    }
-
-    String prefix = getIntegerPrefix(string);
-    String digits;
-    if (prefix == null) {
-      // Nothing to strip. Infer base 10 if autodetection was requested (base == 0).
-      digits = string;
-      if (base == 0) {
-        if (string.length() > 1 && string.startsWith("0")) {
-          // We don't infer the base when input starts with '0' (due
-          // to confusion between octal and decimal).
-          throw new NumberFormatException(
-              "cannot infer base when string begins with a 0: " + Starlark.repr(stringForErrors));
-        }
-        base = 10;
-      }
-    } else {
-      // Strip prefix. Infer base from prefix if unknown (base == 0), or else verify its
-      // consistency.
-      digits = string.substring(prefix.length());
-      int expectedBase = INT_PREFIXES.get(prefix);
-      if (base == 0) {
-        base = expectedBase;
-      } else if (base != expectedBase) {
-        throw new NumberFormatException(
-            String.format(
-                "invalid base-%d literal: %s (%s prefix wants base %d)",
-                base, Starlark.repr(stringForErrors), prefix, expectedBase));
-      }
-    }
-
-    if (base < 2 || base > 36) {
-      throw new NumberFormatException(
-          String.format("invalid base %d (want 2 <= base <= 36)", base));
-    }
-    StarlarkInt result;
-    try {
-      result = StarlarkInt.of(Long.parseLong(digits, base));
-    } catch (NumberFormatException unused1) {
-      try {
-        result = StarlarkInt.of(new BigInteger(digits, base));
-      } catch (NumberFormatException unused2) {
-        throw new NumberFormatException(
-            String.format("invalid base-%d literal: %s", base, Starlark.repr(stringForErrors)));
-      }
-    }
-    return isNegative ? StarlarkInt.uminus(result) : result;
-  }
-
-  @Nullable
-  private static String getIntegerPrefix(String value) {
-    value = Ascii.toLowerCase(value);
-    for (String prefix : INT_PREFIXES.keySet()) {
-      if (value.startsWith(prefix)) {
-        return prefix;
-      }
-    }
-    return null;
-  }
-
   @StarlarkMethod(
       name = "dict",
       doc =
diff --git a/src/main/java/net/starlark/java/eval/StarlarkInt.java b/src/main/java/net/starlark/java/eval/StarlarkInt.java
index 1cc6041..f70f38e 100644
--- a/src/main/java/net/starlark/java/eval/StarlarkInt.java
+++ b/src/main/java/net/starlark/java/eval/StarlarkInt.java
@@ -81,10 +81,85 @@
 
   /**
    * Returns the int denoted by a literal string in the specified base, as if by the Starlark
-   * expression {@code int(str, base)}.
+   * expression {@code int(s, base)}.
+   *
+   * @throws NumberFormatException if the input is invalid.
    */
-  public static StarlarkInt parse(String str, int base) throws NumberFormatException {
-    return MethodLibrary.parseInt(str, base);
+  public static StarlarkInt parse(String s, int base) {
+    String stringForErrors = s;
+
+    if (s.isEmpty()) {
+      throw new NumberFormatException("empty string");
+    }
+
+    // +/- prefix?
+    boolean isNegative = false;
+    char c = s.charAt(0);
+    if (c == '+') {
+      s = s.substring(1);
+    } else if (c == '-') {
+      s = s.substring(1);
+      isNegative = true;
+    }
+
+    String digits = s;
+
+    // 0b 0o 0x prefix?
+    if (s.length() > 1 && s.charAt(0) == '0') {
+      int prefixBase = 0;
+      c = s.charAt(1);
+      if (c == 'b' || c == 'B') {
+        prefixBase = 2;
+      } else if (c == 'o' || c == 'O') {
+        prefixBase = 8;
+      } else if (c == 'x' || c == 'X') {
+        prefixBase = 16;
+      }
+      if (prefixBase != 0) {
+        digits = s.substring(2); // strip prefix
+        if (base == 0) {
+          base = prefixBase;
+        } else if (base != prefixBase) {
+          throw new NumberFormatException(
+              String.format(
+                  "invalid base-%d literal: %s (%s prefix wants base %d)",
+                  base, Starlark.repr(stringForErrors), s.substring(0, 2), prefixBase));
+        }
+      }
+    }
+
+    // No prefix, no base? Use decimal.
+    if (digits == s && base == 0) {
+      // Don't infer base when input starts with '0' due to octal/decimal ambiguity.
+      if (s.length() > 1 && s.charAt(0) == '0') {
+        throw new NumberFormatException(
+            "cannot infer base when string begins with a 0: " + Starlark.repr(stringForErrors));
+      }
+      base = 10;
+    }
+    if (base < 2 || base > 36) {
+      throw new NumberFormatException(
+          String.format("invalid base %d (want 2 <= base <= 36)", base));
+    }
+
+    // Do not allow Long.parseLong and new BigInteger to accept another +/- sign.
+    if (digits.startsWith("+") || digits.startsWith("-")) {
+      throw new NumberFormatException(
+          String.format("invalid base-%d literal: %s", base, Starlark.repr(stringForErrors)));
+    }
+
+    StarlarkInt result;
+    try {
+      result = StarlarkInt.of(Long.parseLong(digits, base));
+    } catch (NumberFormatException unused1) {
+      try {
+        result = StarlarkInt.of(new BigInteger(digits, base));
+      } catch (NumberFormatException unused2) {
+        throw new NumberFormatException(
+            String.format("invalid base-%d literal: %s", base, Starlark.repr(stringForErrors)));
+      }
+    }
+    return isNegative ? StarlarkInt.uminus(result) : result;
   }
 
   // Subclass for values exactly representable in a Java int.
@@ -216,13 +291,17 @@
   /** Returns this StarlarkInt as a string of decimal digits. */
   @Override
   public String toString() {
-    // TODO(adonovan): opt: avoid Number allocation
-    return toNumber().toString();
+    if (this instanceof Int32) {
+      return Integer.toString(((Int32) this).v);
+    } else if (this instanceof Int64) {
+      return Long.toString(((Int64) this).v);
+    } else {
+      return toBigInteger().toString();
+    }
   }
 
   @Override
   public void repr(Printer printer) {
-    // TODO(adonovan): opt: avoid Number and String allocations.
     printer.append(toString());
   }
 
diff --git a/src/test/java/net/starlark/java/eval/testdata/int_constructor.star b/src/test/java/net/starlark/java/eval/testdata/int_constructor.star
index 973cc15..9d4cc54 100644
--- a/src/test/java/net/starlark/java/eval/testdata/int_constructor.star
+++ b/src/test/java/net/starlark/java/eval/testdata/int_constructor.star
@@ -64,6 +64,7 @@
 assert_eq(int('016', 8), 14)
 assert_eq(int('016', 16), 22)
 assert_eq(int('0', 0), 0)
+assert_eq(int('0x0b10', 16), 0x0b10)
 int('0xFF', 8) ### invalid base-8 literal: "0xFF"
 ---
 int('016', 0) ### cannot infer base when string begins with a 0: "016"
@@ -103,3 +104,7 @@
 int('1.5') ### invalid base-10 literal: "1.5"
 ---
 int('ab') ### invalid base-10 literal: "ab"
+---
+int('--1') ### invalid base-10 literal: "--1"
+---
+int('-0x-10', 16) ### invalid base-16 literal: "-0x-10"