Fix toString dispatch for subtypes of native JsTypes.

Change-Id: If5b8dbfd75c41e49c8dd0dd7b6ff36bbd143d73d
diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/RuntimeConstants.java b/dev/core/src/com/google/gwt/dev/jjs/ast/RuntimeConstants.java
index 5fc3c2b..15f5b8e 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/ast/RuntimeConstants.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/ast/RuntimeConstants.java
@@ -92,6 +92,7 @@
   public static final String RUNTIME_GET_CLASS_PROTOTYPE = "Runtime.getClassPrototype";
   public static final String RUNTIME_MAKE_LAMBDA_FUNCTION = "Runtime.makeLambdaFunction";
   public static final String RUNTIME_PROVIDE = "Runtime.provide";
+  public static final String RUNTIME_TO_STRING = "Runtime.toString";
   public static final String RUNTIME_TYPE_MARKER_FN = "Runtime.typeMarkerFn";
   public static final String RUNTIME_UNIQUE_ID = "Runtime.uniqueId";
 
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/Devirtualizer.java b/dev/core/src/com/google/gwt/dev/jjs/impl/Devirtualizer.java
index 5bd2725..d0ffe59 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/Devirtualizer.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/Devirtualizer.java
@@ -245,8 +245,12 @@
       // and optimizations need to be strong enough to perform the same kind of size reductions
       // achieved by keeping track of singleImpls.
 
-      //
-      if (!program.typeOracle.isDualJsoInterface(targetType) &&
+      if (method.getSignature().equals("toString()Ljava/lang/String;")) {
+        // Object.toString is special because: 1) every JS object has it and 2) GWT creates
+        // a bridge from toString to its implementation method.
+        devirtualMethodByMethod.put(
+            method, program.getIndexedMethod(RuntimeConstants.RUNTIME_TO_STRING));
+      } else if (!program.typeOracle.isDualJsoInterface(targetType) &&
           program.typeOracle.isSingleJsoImpl(targetType)) {
         // Optimize the trampoline away when there is ONLY JSO dispatch.
         // TODO(rluble): verify that this case can not arise in optimized mode and if so
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallTightener.java b/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallTightener.java
index ec80e56..5ccf347 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallTightener.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallTightener.java
@@ -72,33 +72,57 @@
         return;
       }
 
-      JMethodCall newCall = maybeUpgradeToNonPolymorphicCall(x);
-
-      // If the call is still polymorphic, try tightening the method.
-      if (newCall.canBePolymorphic()) {
-        newCall = maybeTightenMethodCall(newCall);
+      JMethod mostSpecificTarget = getMostSpecificOverride(x);
+      if (mostSpecificTarget.getEnclosingType().isJsNative()) {
+        // Never tighten to instance methods in native types. This done because java.lang.Object
+        // methods are implicitly implemented by all objects but may or may not be present in the
+        // native type implementation. The dispatch for these is eventually done through a
+        // trampoline {@see Devirtualizer} that makes the proper checks and invokes the native
+        // implementation if present.
+        assert x.getTarget().getEnclosingType().isJavaLangObject()
+            || x.getTarget().getEnclosingType().isJsNative();
+        return;
       }
 
+      // Tighten the method call if a more specific override is available.
+      JMethodCall newCall = maybeReplaceTargetMethod(x, mostSpecificTarget);
+      maybeUpgradeToNonPolymorphicCall(newCall);
+
       if (newCall != x) {
         ctx.replaceMe(newCall);
       }
     }
 
-    private JMethodCall maybeUpgradeToNonPolymorphicCall(JMethodCall x) {
+    private JMethod getMostSpecificOverride(final JMethodCall methodCall) {
+      JMethod original = methodCall.getTarget();
+      JClassType underlyingType =
+          (JClassType) methodCall.getInstance().getType().getUnderlyingType();
+
+      return program.typeOracle.findMostSpecificOverride(underlyingType, original);
+    }
+
+    private JMethodCall maybeReplaceTargetMethod(JMethodCall methodCall, JMethod newTargetMethod) {
+      if (methodCall.getTarget() == newTargetMethod) {
+        return methodCall;
+      }
+      return new JMethodCall(
+          methodCall.getSourceInfo(),
+          methodCall.getInstance(),
+          newTargetMethod,
+          methodCall.getArgs());
+    }
+
+    private void maybeUpgradeToNonPolymorphicCall(JMethodCall x) {
       JReferenceType instanceType = (JReferenceType) x.getInstance().getType();
 
-      if (!instanceType.canBeSubclass()) {
-        // Mark a call as non-polymorphic if the targeted type is guaranteed to be not a subclass.
-        x = maybeTightenMethodCall(x);
-        x.setCannotBePolymorphic();
-        madeChanges();
-      } else if (!hasPotentialOverride(instanceType, x.getTarget())) {
-        // Mark a call as non-polymorphic if the targeted method is the only possible dispatch.
+      if (!instanceType.canBeSubclass() || !hasPotentialOverride(instanceType, x.getTarget())) {
+        assert getMostSpecificOverride(x) == x.getTarget();
+
+        // Mark a call as non-polymorphic if the targeted type is guaranteed to be not a subclass
+        // or there are no overriding implementations.
         x.setCannotBePolymorphic();
         madeChanges();
        }
-
-      return x;
     }
 
     private boolean hasPotentialOverride(JReferenceType instanceType, JMethod target) {
@@ -117,27 +141,6 @@
       return false;
     }
 
-    private JMethodCall maybeTightenMethodCall(final JMethodCall methodCall) {
-      JMethod original = methodCall.getTarget();
-      JClassType underlyingType =
-          (JClassType) methodCall.getInstance().getType().getUnderlyingType();
-
-      JMethod mostSpecificOverride =
-          program.typeOracle.findMostSpecificOverride(underlyingType, original);
-
-      if (mostSpecificOverride == original
-          // Never tighten object methods to native implementations. This decision forces
-          // the use of the Object trampoline for hashcCode, equals and toString.
-          || (original.getEnclosingType().isJavaLangObject()
-              && mostSpecificOverride.isJsNative())) {
-        return methodCall;
-      }
-      JMethodCall newCall = new JMethodCall(
-          methodCall.getSourceInfo(), methodCall.getInstance(), mostSpecificOverride);
-      newCall.addArgs(methodCall.getArgs());
-      return newCall;
-    }
-
     @Override
     public void endVisit(JNewInstance x, Context ctx) {
       // Do not tighten new operations.
diff --git a/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/Runtime.java b/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/Runtime.java
index e48652b..c7c7606 100644
--- a/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/Runtime.java
+++ b/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/Runtime.java
@@ -212,4 +212,11 @@
     }
     Object.defineProperties(proto,  propertyDefinition);
   }-*/;
+
+  public static native String toString(Object object) /*-{
+    if (Array.isArray(object) && @Util::hasTypeMarker(*)(object)) {
+       return @Object::toString(Ljava/lang/Object;)(object);
+    }
+    return object.toString();
+  }-*/;
 }
diff --git a/dev/core/test/com/google/gwt/dev/javac/typemodel/JParameterizedTypeTest.java b/dev/core/test/com/google/gwt/dev/javac/typemodel/JParameterizedTypeTest.java
index 1180055..34cba2a 100644
--- a/dev/core/test/com/google/gwt/dev/javac/typemodel/JParameterizedTypeTest.java
+++ b/dev/core/test/com/google/gwt/dev/javac/typemodel/JParameterizedTypeTest.java
@@ -433,7 +433,8 @@
     expectedMethods.addAll(expected);
     if (addObjectMethods) {
       TypeOracle oracle = moduleContext.getOracle();
-      expectedMethods.addAll(Arrays.asList(oracle.getJavaLangObject().getMethods()));
+      expectedMethods.addAll(
+          Arrays.asList(oracle.getJavaLangObject().getOverridableMethods()));
     }
 
     for (JMethod method : actual) {
diff --git a/dev/core/test/com/google/gwt/dev/jjs/JavaAstConstructor.java b/dev/core/test/com/google/gwt/dev/jjs/JavaAstConstructor.java
index 3bf511a..7e5f7bc 100644
--- a/dev/core/test/com/google/gwt/dev/jjs/JavaAstConstructor.java
+++ b/dev/core/test/com/google/gwt/dev/jjs/JavaAstConstructor.java
@@ -292,6 +292,7 @@
                   "  public static void bootstrap() {}",
                   "  public static void emptyMethod() {}",
                   "  public static void getClassPrototype() {}",
+                  "  public static String toString(Object object) { return null; }",
                   "  static native void typeMarkerFn() /*-{}-*/;",
                   "}");
         }
diff --git a/dev/core/test/com/google/gwt/dev/jjs/impl/DevirtualizerTest.java b/dev/core/test/com/google/gwt/dev/jjs/impl/DevirtualizerTest.java
index ccc1c71..88bd4f5 100644
--- a/dev/core/test/com/google/gwt/dev/jjs/impl/DevirtualizerTest.java
+++ b/dev/core/test/com/google/gwt/dev/jjs/impl/DevirtualizerTest.java
@@ -151,8 +151,78 @@
     result.intoString(expected);
   }
 
+  public void testDevirtualizeObjectMethods() throws UnableToCompleteException {
+    addSnippetImport("jsinterop.annotations.JsType");
+    addSnippetImport("jsinterop.annotations.JsOverlay");
+    addSnippetClassDecl(
+        "@JsType(isNative=true) public static class NativeClass {",
+        "}",
+        "public static class NativeClassSubclass extends NativeClass {",
+        "}");
+
+    String code = Joiner.on('\n').join(
+        "NativeClass nativeClass = null;",
+        "nativeClass.toString();",
+        "nativeClass.equals(nativeClass);",
+        "nativeClass.hashCode();",
+        "NativeClassSubclass subclass = null;",
+        "subclass.toString();",
+        "subclass.equals(subclass);",
+        "subclass.hashCode();"
+        );
+
+    String expected = Joiner.on('\n').join(
+        "EntryPoint$NativeClass nativeClass = null;",
+        "Runtime.toString(nativeClass);",
+        "Object.equals_Ljava_lang_Object__Z__devirtual$(nativeClass, nativeClass);",
+        "Object.hashCode__I__devirtual$(nativeClass);",
+        "EntryPoint$NativeClassSubclass subclass = null;",
+        "Runtime.toString(subclass);",
+        "Object.equals_Ljava_lang_Object__Z__devirtual$(subclass, subclass);",
+        "Object.hashCode__I__devirtual$(subclass);");
+    Result result = optimize("void", code);
+    result.intoString(expected);
+  }
+
+  public void testDevirtualizeObjectMethodsExplicitelyDefined() throws UnableToCompleteException {
+    addSnippetImport("jsinterop.annotations.JsType");
+    addSnippetImport("jsinterop.annotations.JsOverlay");
+    addSnippetClassDecl(
+        "@JsType(isNative=true) public static class NativeClass {",
+        "  public native String toString();",
+        "  public native int hashCode();",
+        "  public native boolean equals(Object o);",
+        "}",
+        "public static class NativeClassSubclass extends NativeClass {",
+        "}");
+
+    String code = Joiner.on('\n').join(
+        "NativeClass nativeClass = null;",
+        "nativeClass.toString();",
+        "nativeClass.equals(nativeClass);",
+        "nativeClass.hashCode();",
+        "NativeClassSubclass subclass = null;",
+        "subclass.toString();",
+        "subclass.equals(subclass);",
+        "subclass.hashCode();"
+    );
+
+    String expected = Joiner.on('\n').join(
+        "EntryPoint$NativeClass nativeClass = null;",
+        "Runtime.toString(nativeClass);",
+        "Object.equals_Ljava_lang_Object__Z__devirtual$(nativeClass, nativeClass);",
+        "Object.hashCode__I__devirtual$(nativeClass);",
+        "EntryPoint$NativeClassSubclass subclass = null;",
+        "Runtime.toString(subclass);",
+        "Object.equals_Ljava_lang_Object__Z__devirtual$(subclass, subclass);",
+        "Object.hashCode__I__devirtual$(subclass);");
+    Result result = optimize("void", code);
+    result.intoString(expected);
+  }
+
   @Override
   protected boolean doOptimizeMethod(TreeLogger logger, JProgram program, JMethod method) {
+    ReplaceCallsToNativeJavaLangObjectOverrides.exec(program);
     Devirtualizer.exec(program);
     return true;
   }
diff --git a/user/super/com/google/gwt/emul/java/lang/Object.java b/user/super/com/google/gwt/emul/java/lang/Object.java
index 523beba..3dc8582 100644
--- a/user/super/com/google/gwt/emul/java/lang/Object.java
+++ b/user/super/com/google/gwt/emul/java/lang/Object.java
@@ -77,9 +77,12 @@
   }
 
   public String toString() {
-    return getClass().getName() + '@' + Integer.toHexString(hashCode());
+    return toString(this);
   }
 
+  private static String toString(Object object) {
+    return object.getClass().getName() + '@' + Integer.toHexString(object.hashCode());
+  }
   /**
    * Never called; here for JRE compatibility.
    *
diff --git a/user/test/com/google/gwt/core/interop/NativeJsTypeTest.java b/user/test/com/google/gwt/core/interop/NativeJsTypeTest.java
index b222732..4cf5378 100644
--- a/user/test/com/google/gwt/core/interop/NativeJsTypeTest.java
+++ b/user/test/com/google/gwt/core/interop/NativeJsTypeTest.java
@@ -471,9 +471,15 @@
 
   @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Error")
   private static class NativeError {
+    public String message;
   }
 
+  private static final String ERROR_FROM_NATIVE_ERROR_SUBCLASS = "error from NativeErrorSubclass";
+
   private static class NativeErrorSubclass extends NativeError {
+    public NativeErrorSubclass() {
+      message = ERROR_FROM_NATIVE_ERROR_SUBCLASS;
+    }
   }
 
   public void testObjectPropertiesAreCopied() {
@@ -482,8 +488,6 @@
     // Make sure the subclass is a proper Java object (the typeMarker should be one of the
     // properties copied from java.lang.Object).
     assertFalse(error instanceof JavaScriptObject);
-    // TODO(rluble): NativeErrorSubclass should have inherited Error toString behavior not
-    // j.l.Object.toString behavior.
-    // assertTrue(error.toString().matches("[0-9a-zA-Z$_.]+@[0-9a-fA-F]+"));
+    assertTrue(error.toString().contains(ERROR_FROM_NATIVE_ERROR_SUBCLASS));
   }
 }