Allow calls to devirtualized methods from JSNI.

Only disallow taking references to devirtualized methods but
allow invocations.

Bug: #9356
Bug-Link: http://github.com/gwtproject/gwt/issues/9356
Change-Id: I98d3248aedc705439db737b2d13e5bbe28469b5f
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 795a91c..67d5e4d 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
@@ -36,15 +36,25 @@
 import com.google.gwt.dev.jjs.ast.JTypeOracle;
 import com.google.gwt.dev.jjs.ast.JVariableRef;
 import com.google.gwt.dev.jjs.ast.RuntimeConstants;
+import com.google.gwt.dev.jjs.ast.js.JsniMethodBody;
+import com.google.gwt.dev.jjs.ast.js.JsniMethodRef;
 import com.google.gwt.dev.jjs.impl.MakeCallsStatic.CreateStaticImplsVisitor;
 import com.google.gwt.dev.jjs.impl.MakeCallsStatic.StaticCallConverter;
+import com.google.gwt.dev.js.ast.JsContext;
+import com.google.gwt.dev.js.ast.JsInvocation;
+import com.google.gwt.dev.js.ast.JsModVisitor;
+import com.google.gwt.dev.js.ast.JsNameRef;
+import com.google.gwt.thirdparty.guava.common.collect.Iterables;
 import com.google.gwt.thirdparty.guava.common.collect.Lists;
 import com.google.gwt.thirdparty.guava.common.collect.Maps;
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
 
+import java.util.Collections;
 import java.util.EnumMap;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Devirtualization is the process of converting virtual method calls on instances that might be
@@ -104,6 +114,32 @@
     }
 
     @Override
+    public void endVisit(JsniMethodRef x, Context ctx) {
+      JMethod method = x.getTarget();
+      if (method == null || !mightNeedDevirtualization(method)) {
+        return;
+      }
+      ensureDevirtualVersionExists(method);
+
+      // Replace the JMethod in jsni reference to a reference to the devirtualized method.
+      // Note that a JsniMethodRefs is a pair containing the Jsni reference as text (i.e.
+      // "@java.lang.Boolean::booleanValue") and a reference to the actual JMethod in the AST;
+      // in generation time the actual JMethod that is called is looked up from the reference text.
+      //
+      // Here we just replace the JMethod the reference is pointing to without updating the
+      // reference text.
+      //
+      // Keeping the "key" unchanged avoid the necessity to sync up with the modifications in the
+      // JS AST when the JsniMethodBody is processed.
+      JMethod devirtualMethod = devirtualMethodByMethod.get(method);
+      ctx.replaceMe(new JsniMethodRef(
+          x.getSourceInfo(),
+          x.getIdent(),
+          devirtualMethod,
+          program.getJavaScriptObject()));
+    }
+
+    @Override
     public void endVisit(JMethodCall x, Context ctx) {
       JMethod method = x.getTarget();
       if (!method.needsDynamicDispatch()) {
@@ -137,6 +173,57 @@
       return true;
     }
 
+    @Override
+    public boolean visit(JsniMethodBody x, Context ctx) {
+      final Set<String> devirtualMethodJsniIdentifiers = Sets.newHashSet();
+      for (JsniMethodRef jsniMethodRef : x.getJsniMethodRefs()) {
+        JMethod target = jsniMethodRef.getTarget();
+        if (target != null && mightNeedDevirtualization(target)) {
+          devirtualMethodJsniIdentifiers.add(jsniMethodRef.getIdent());
+        }
+      }
+
+      // Devirtualize jsni method calls.
+      new JsModVisitor() {
+        @Override
+        public void endVisit(JsInvocation x, JsContext ctx) {
+          if (!(x.getQualifier() instanceof JsNameRef)) {
+            // If the invocation does not have a name as a qualifier then it is an expression and
+            // cannot be a jsni method reference.
+            return;
+          }
+          JsNameRef nameRef = (JsNameRef) x.getQualifier();
+          if (!nameRef.isJsniReference()) {
+            // The invocation is not to a JSNI method.
+            return;
+          }
+
+          // Retrieve the method referred by the JsniMethodRef and check whether it needs
+          // devirtualization.
+          if (!devirtualMethodJsniIdentifiers.contains(nameRef.getIdent())) {
+            return;
+          }
+          // Devirtualize method by rewriting
+          // a.@java.lang.Boolean::booleanValue() ==> @java.lang.Boolean::booleanValue(a).
+          //
+          // Not the the reference identifier is *NOT* changed and will act a the key in the lookup
+          // for the corresponding JMethod which is contained in the corresponding JsniMethodRef
+          // node.
+          ctx.replaceMe(
+              new JsInvocation(
+                  x.getSourceInfo(),
+                  new JsNameRef(
+                      nameRef.getSourceInfo(), nameRef.getIdent()),
+                  Iterables.concat(
+                      Collections.singleton(nameRef.getQualifier()), x.getArguments())));
+          return;
+        }
+      }.accept(x.getFunc());
+
+      // Now go ahead and fix the corresponding JSNI references.
+      return true;
+    }
+
     /**
      * Constructs and caches a method that is a new static version of the given method or a
      * trampoline function that wraps a new static version of the given method. It chooses which to
@@ -176,10 +263,7 @@
             staticImplCreator.getOrCreateStaticImpl(program, overridingMethod);
         devirtualMethodByMethod.put(method, jsoStaticImpl);
       } else if (isOverlayMethod(method)) {
-        // A virtual dispatch on a target that is already known to be a JavaScriptObject, this
-        // should have been handled by MakeCallsStatic.
-        // TODO(rluble): verify that this case can not arise in optimized mode and if so
-        // remove as is an unnecessary optimization.
+        // A virtual dispatch on a target that is already known to be an overlay method,.
         JMethod devirtualMethod = staticImplCreator.getOrCreateStaticImpl(program, method);
         devirtualMethodByMethod.put(method, devirtualMethod);
       } else {
@@ -193,7 +277,6 @@
     }
 
     private boolean mightNeedDevirtualization(JMethod method, JReferenceType instanceType) {
-      // todo remove instance check
       if (instanceType == null || !method.needsDynamicDispatch()) {
         return false;
       }
@@ -207,6 +290,9 @@
         // Methods in a native JsType that are not JsOverlay should NOT be devirtualized.
         return false;
       }
+      if (instanceType.isNullType()) {
+        instanceType = method.getEnclosingType();
+      }
       EnumSet<DispatchType> dispatchType = program.getDispatchType(instanceType);
       dispatchType.remove(DispatchType.HAS_JAVA_VIRTUAL_DISPATCH);
       return !dispatchType.isEmpty();
@@ -233,7 +319,7 @@
    * Maps each Object instance methods (ie, {@link Object#equals(Object)}) onto
    * its corresponding devirtualizing method.
    */
-  protected Map<JMethod, JMethod> devirtualMethodByMethod = Maps.newHashMap();
+  private Map<JMethod, JMethod> devirtualMethodByMethod = Maps.newHashMap();
 
   /**
    * Contains the Cast.hasJavaObjectVirtualDispatch method.
@@ -311,8 +397,7 @@
       return;
     }
 
-    RewriteVirtualDispatches rewriter = new RewriteVirtualDispatches();
-    rewriter.accept(program);
+    new RewriteVirtualDispatches().accept(program);
   }
 
   /**
@@ -554,4 +639,8 @@
           staticImplCreator.getOrCreateStaticImpl(program, overridingMethod));
     }
   }
+
+  private static String getJsniReferenceIdentifier(JMethod method) {
+    return "@" + method.getJsniSignature(true, false);
+  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
index 4a24c45..b076ab3 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
@@ -1508,15 +1508,15 @@
 
     @Override
     public JsFunction transformJsniMethodBody(JsniMethodBody jsniMethodBody) {
-      final Map<String, JNode> jsniMap = Maps.newHashMap();
+      final Map<String, JNode> nodeByJsniReference = Maps.newHashMap();
       for (JsniClassLiteral ref : jsniMethodBody.getClassRefs()) {
-        jsniMap.put(ref.getIdent(), ref.getField());
+        nodeByJsniReference.put(ref.getIdent(), ref.getField());
       }
       for (JsniFieldRef ref : jsniMethodBody.getJsniFieldRefs()) {
-        jsniMap.put(ref.getIdent(), ref.getField());
+        nodeByJsniReference.put(ref.getIdent(), ref.getField());
       }
       for (JsniMethodRef ref : jsniMethodBody.getJsniMethodRefs()) {
-        jsniMap.put(ref.getIdent(), ref.getTarget());
+        nodeByJsniReference.put(ref.getIdent(), ref.getTarget());
       }
 
       final JsFunction function = jsniMethodBody.getFunc();
@@ -1549,7 +1549,7 @@
 
           // Replace invocation to ctor with a new op.
           String ident = ref.getIdent();
-          JNode node = jsniMap.get(ident);
+          JNode node = nodeByJsniReference.get(ident);
           assert node instanceof JConstructor;
           assert ref.getQualifier() == null;
           JsName jsName = names.get(node);
@@ -1567,7 +1567,7 @@
           }
 
           String ident = x.getIdent();
-          JNode node = jsniMap.get(ident);
+          JNode node = nodeByJsniReference.get(ident);
           assert (node != null);
           if (node instanceof JField) {
             JField field = (JField) node;
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JsniRestrictionChecker.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JsniRestrictionChecker.java
index 631213f..8646b71 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JsniRestrictionChecker.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JsniRestrictionChecker.java
@@ -22,9 +22,17 @@
 import com.google.gwt.dev.jjs.ast.JMethodBody;
 import com.google.gwt.dev.jjs.ast.JProgram;
 import com.google.gwt.dev.jjs.ast.JVisitor;
+import com.google.gwt.dev.jjs.ast.js.JsniMethodBody;
 import com.google.gwt.dev.jjs.ast.js.JsniMethodRef;
+import com.google.gwt.dev.js.ast.JsContext;
+import com.google.gwt.dev.js.ast.JsExpression;
+import com.google.gwt.dev.js.ast.JsInvocation;
+import com.google.gwt.dev.js.ast.JsModVisitor;
+import com.google.gwt.dev.js.ast.JsNameRef;
+import com.google.gwt.thirdparty.guava.common.collect.Maps;
 import com.google.gwt.thirdparty.guava.common.collect.Sets;
 
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -51,39 +59,82 @@
       }
 
       @Override
-      public boolean visit(JsniMethodRef x, Context ctx) {
-        checkJsniMethodReference(x);
-        return true;
+      public boolean visit(final JsniMethodBody x, Context ctx) {
+        final Map<String, JsniMethodRef> methodsByJsniReference = Maps.newHashMap();
+        for (JsniMethodRef ref : x.getJsniMethodRefs()) {
+          methodsByJsniReference.put(ref.getIdent(), ref);
+        }
+        if (methodsByJsniReference.isEmpty()) {
+          return false;
+        }
+
+        // Examine the JS AST that represents the JSNI method body to check for devirtualizable
+        // methods references that are not directly called.
+        new JsModVisitor() {
+          @Override
+          public boolean visit(JsInvocation x, JsContext ctx) {
+            if (!(x.getQualifier() instanceof JsNameRef)) {
+              // If the invocation does not have a name as a qualifier (it might be an
+              // expression), the it is certainly not a JSNI method reference; but it might
+              // contain one so explore its subnodes the usual way.
+              return true;
+            }
+            JsNameRef ref = (JsNameRef) x.getQualifier();
+            if (!ref.isJsniReference()) {
+              // The invocation is not to a JSNI method; but its subnodes might contain one
+              // hence explore them the usual way.
+              return true;
+            }
+
+            // Skip the method JsNameRef but check the qualifier.
+            JsExpression methodQualifier = ref.getQualifier();
+            if (methodQualifier != null) {
+              // Even if it is a direct call, there might be a reference in the qualifier.
+              accept(methodQualifier);
+            }
+
+            // This is a direct call so if it was a JSNI reference to a devirtualized method
+            // it is safe, as it will be rewritten by {@see Devirtualizer}.
+            return false;
+          }
+
+          @Override
+          public void endVisit(JsNameRef x, JsContext ctx) {
+            JsniMethodRef jsniMethodReference = methodsByJsniReference.get(x.getIdent());
+            if (jsniMethodReference != null) {
+              // This is a JSNI reference that is not in a direct call, so check if it is valid.
+              checkJsniMethodReference(jsniMethodReference);
+            }
+          }
+        }.accept(x.getFunc());
+        return false;
       }
 
       private void checkJsniMethodReference(JsniMethodRef jsniMethodReference) {
         JMethod method = jsniMethodReference.getTarget();
         JDeclaredType enclosingType = method.getEnclosingType();
 
-        if (isNonStaticJsoClassDispatch(method, enclosingType)) {
+        if (isNonStaticJsoClassDispatch(method, enclosingType)
+            || isJsoInterface(enclosingType)) {
           logError(jsniMethodReference,
-              "Cannot call non-static method %s on an instance which is a "
-                  + "subclass of JavaScriptObject. Only static method calls on JavaScriptObject "
-                  + "subclasses are allowed in JSNI.",
-              getDescription(method));
-        } else if (isJsoInterface(enclosingType)) {
-          logError(jsniMethodReference,
-              "Cannot call method %s on an instance which might be a JavaScriptObject. "
-                  + "Such a method call is only allowed in pure Java (non-JSNI) functions.",
+              "Method %s is implemented by a JSO and can only be used in calls "
+                  + "within a JSNI method body.",
               getDescription(method));
         } else if (program.isRepresentedAsNativeJsPrimitive(enclosingType)
             && !method.isStatic()
             && !method.isConstructor()) {
           logError(jsniMethodReference,
-              "Cannot call method %s. Instance methods on %s cannot be called from JSNI.",
+              "Method %s is implemented by devirtualized type %s JSO and can only be used in "
+                  + "calls within a JSNI method body.",
               getDescription(method),
               getDescription(enclosingType));
         } else if (typesRequiringTrampolineDispatch.contains(enclosingType)
             && !method.isStatic()
             && !method.isConstructor()) {
           logWarning(jsniMethodReference,
-              "Unsafe call to method %s. Instance methods from %s should "
-                  + "not be called on Boolean, Double, String, Array or JSO instances from JSNI.",
+              "Unsafe reference to method %s. Instance methods from %s should "
+                  + "not be called on Boolean, Double, String, Array or JSO instances "
+                  + "from  within a JSNI method body.",
               getDescription(method),
               getDescription(enclosingType));
         }
diff --git a/dev/core/test/com/google/gwt/dev/jjs/impl/JsniRestrictionCheckerTest.java b/dev/core/test/com/google/gwt/dev/jjs/impl/JsniRestrictionCheckerTest.java
index 9aabbf9..48cd201 100644
--- a/dev/core/test/com/google/gwt/dev/jjs/impl/JsniRestrictionCheckerTest.java
+++ b/dev/core/test/com/google/gwt/dev/jjs/impl/JsniRestrictionCheckerTest.java
@@ -25,53 +25,67 @@
  */
 public class JsniRestrictionCheckerTest extends OptimizerTestBase {
 
-  public void testConstructorsOnDevirtualizedTypesSucceeds() throws Exception {
-    addSnippetImport("com.google.gwt.core.client.JavaScriptObject");
+  public void testInvocationInDevirtualizedTypesSucceeds() throws Exception {
     addSnippetClassDecl(
         "static class Buggy {",
+        "  interface IBar {",
+        "    void bar();",
+        "  }",
+        "  static final class Foo ",
+        "      extends com.google.gwt.core.client.JavaScriptObject implements IBar {",
+        "    protected Foo() { };",
+        "    void foo() { };",
+        "    public void bar() { };",
+        "    static void staticFoo() { };",
+        "  }",
         "  native void jsniMethod(Object o) /*-{",
         "    @java.lang.Double::new(D)();",
         "    @java.lang.Boolean::new(Z)();",
+        "    new Object().@java.lang.Number::doubleValue()();",
+        "    new Object().@java.lang.Double::doubleValue()();",
+        "    new Object().@Buggy.Foo::foo()();",
+        "    new Object().@Buggy.IBar::bar()();",
+        "    @Buggy.Foo::staticFoo()();",
         "  }-*/;",
         "}");
-
     assertCompileSucceeds("new Buggy().jsniMethod(null);");
   }
 
-  public void testInstanceCallToDevirtualizedFails() throws Exception {
+  public void testReferenceToDevirtualizedInstanceMethodFails() throws Exception {
     addSnippetImport("com.google.gwt.core.client.JavaScriptObject");
     addSnippetClassDecl(
         "static class Buggy {",
         "  native void jsniMethod(Object o) /*-{",
-        "    new Object().@java.lang.Double::doubleValue()();",
+        "    var a = new Object().@java.lang.Double::doubleValue();",
         "  }-*/;",
         "}");
 
     assertCompileFails("new Buggy().jsniMethod(null);",
-        "Line 6: Cannot call method 'double Double.doubleValue()'. Instance methods on 'Double' "
-            + "cannot be called from JSNI.");
+        "Line 6: Method 'double Double.doubleValue()' is implemented by devirtualized "
+            + "type 'Double' JSO and can only be used in calls within a JSNI method body.");
   }
 
-  public void testInstanceCallToTrampolineWarns() throws Exception {
+  public void testReferenceToTrampolineWarns() throws Exception {
     addSnippetImport("com.google.gwt.core.client.JavaScriptObject");
     addSnippetClassDecl(
         "static class Buggy {",
         "  native void jsniMethod(Object o) /*-{",
-        "    new Object().@java.lang.Number::doubleValue()();",
-        "    new Object().@java.lang.CharSequence::charAt(I)(0);",
-        "    \"Hello\".@java.lang.Object::toString()();",
+        "    var a = new Object().@java.lang.Number::doubleValue();",
+        "    var a = new Object().@java.lang.CharSequence::charAt(I);",
+        "    var a = \"Hello\".@java.lang.Object::toString();",
         "  }-*/;",
         "}");
 
     assertCompileSucceeds("new Buggy().jsniMethod(null);",
-        "Line 6: Unsafe call to method 'double Number.doubleValue()'. Instance methods from "
-            + "'Number' should not be called on Boolean, Double, String, Array or JSO instances "
-            + "from JSNI.",
-        "Line 7: Unsafe call to method 'char CharSequence.charAt(int)'. Instance methods from "
-            + "'CharSequence' should not be called on Boolean, Double, String, Array or JSO "
-            + "instances from JSNI.",
-        "Line 8: Unsafe call to method 'String Object.toString()'. Instance methods from 'Object' "
-            + "should not be called on Boolean, Double, String, Array or JSO instances from JSNI.");
+        "Line 6: Unsafe reference to method 'double Number.doubleValue()'. "
+            + "Instance methods from 'Number' should not be called on Boolean, Double, String, "
+            + "Array or JSO instances from  within a JSNI method body.",
+        "Line 7: Unsafe reference to method 'char CharSequence.charAt(int)'. Instance methods from"
+            + " 'CharSequence' should not be called on Boolean, Double, String, Array or JSO"
+            + " instances from  within a JSNI method body.",
+        "Line 8: Unsafe reference to method 'String Object.toString()'. Instance methods from "
+            + "'Object' should not be called on Boolean, Double, String, Array or JSO instances "
+            + "from  within a JSNI method body.");
   }
 
   public void testStaticJsoDispatchSucceeds() throws Exception {
@@ -102,14 +116,13 @@
         "    public void foo() { };",
         "  }",
         "  native void jsniMethod(Object o) /*-{",
-        "    new Object().@Buggy.IFoo::foo()();",
+        "    var a = new Object().@Buggy.IFoo::foo();",
         "  }-*/;",
         "}");
 
     assertCompileFails("new Buggy().jsniMethod(null);",
-        "Line 13: Cannot call method 'void EntryPoint.Buggy.IFoo.foo()' on an instance which might "
-            + "be a JavaScriptObject. Such a method call is only allowed in pure Java (non-JSNI) "
-            + "functions.");
+        "Line 13: Method 'void EntryPoint.Buggy.IFoo.foo()' is implemented by a JSO and can only "
+            + "be used in calls within a JSNI method body.");
   }
 
   public void testNonstaticJsoDispatchFails() throws Exception {
@@ -117,14 +130,13 @@
     addSnippetClassDecl(
         "static class Buggy {",
         "  native void jsniMethod(Object o) /*-{",
-        "    new Object().@com.google.gwt.core.client.JavaScriptObject::toString()();",
+        "    var a = new Object().@com.google.gwt.core.client.JavaScriptObject::toString();",
         "  }-*/;",
         "}");
 
     assertCompileFails("new Buggy().jsniMethod(null);",
-        "Line 6: Cannot call non-static method 'String JavaScriptObject.toString()' on an instance "
-            + "which is a subclass of JavaScriptObject. Only static method calls on "
-            + "JavaScriptObject subclasses are allowed in JSNI.");
+        "Line 6: Method 'String JavaScriptObject.toString()' is implemented by a JSO and can "
+            + "only be used in calls within a JSNI method body.");
   }
 
   public void testNonstaticJsoSubclassDispatchFails() throws Exception {
@@ -136,14 +148,13 @@
         "    void foo() { };",
         "  }",
         "  native void jsniMethod(Object o) /*-{",
-        "    new Object().@Buggy.Foo::foo()();",
+        "    var a = new Object().@Buggy.Foo::foo();",
         "  }-*/;",
         "}");
 
     assertCompileFails("new Buggy().jsniMethod(null);",
-        "Line 10: Cannot call non-static method 'void EntryPoint.Buggy.Foo.foo()' on an instance "
-            + "which is a subclass of JavaScriptObject. Only static method calls on "
-            + "JavaScriptObject subclasses are allowed in JSNI.");
+        "Line 10: Method 'void EntryPoint.Buggy.Foo.foo()' is implemented by a JSO and can "
+            + "only be used in calls within a JSNI method body.");
   }
 
   public void testStringInstanceMethodCallFail() throws Exception {
@@ -151,13 +162,13 @@
         "static class Buggy {",
         "  static String foo;",
         "  native void jsniMethod(Object o) /*-{",
-        "    \"Hello\".@java.lang.String::length()();",
+        "    var a = \"Hello\".@java.lang.String::length();",
         "  }-*/;",
         "}");
 
     assertCompileFails("new Buggy().jsniMethod(null);",
-        "Line 6: Cannot call method 'int String.length()'. Instance methods on 'String' cannot be "
-            + "called from JSNI.");
+        "Line 6: Method 'int String.length()' is implemented by devirtualized type 'String' "
+            + "JSO and can only be used in calls within a JSNI method body.");
   }
 
   public void testStringStaticMethodCallSucceeds() throws Exception {
@@ -165,7 +176,7 @@
         "static class Buggy {",
         "  static String foo;",
         "  native void jsniMethod(Object o) /*-{",
-        "    @java.lang.String::valueOf(Z)();",
+        "    var a = @java.lang.String::valueOf(Z);",
         "  }-*/;",
         "}");
 
diff --git a/user/test/com/google/gwt/dev/jjs/test/JsniDispatchTest.java b/user/test/com/google/gwt/dev/jjs/test/JsniDispatchTest.java
index ecdd3a5..67f160f 100644
--- a/user/test/com/google/gwt/dev/jjs/test/JsniDispatchTest.java
+++ b/user/test/com/google/gwt/dev/jjs/test/JsniDispatchTest.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.dev.jjs.test;
 
+import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.junit.client.GWTTestCase;
 
 /**
@@ -95,4 +96,51 @@
     assertEquals("Hello from SuperSuperSuperFoo 10 times",
         callSayHelloNTimesAtSuperSuperSuperFoo(new Foo()));
   }
+
+  public native int doubleToInt(Double d) /*-{
+    return d.@java.lang.Double::intValue()();
+  }-*/;
+
+  public native int numberToInt(Number n) /*-{
+    return n.@java.lang.Number::intValue()();
+  }-*/;
+
+  interface DualInterface {
+    int m();
+  }
+
+  private static class JavaImplementor implements DualInterface {
+    public int m() {
+      return 1;
+    }
+  }
+
+  private static class JsoImplementor extends JavaScriptObject implements DualInterface {
+    public final native int m() /*-{
+      return this.a;
+    }-*/;
+
+    protected JsoImplementor() {
+    }
+  }
+
+  public native int callMOnDual(DualInterface i) /*-{
+    return i.@JsniDispatchTest.DualInterface::m()();
+  }-*/;
+
+  public native int callMOnJso(JsoImplementor jso) /*-{
+    return jso.@JsniDispatchTest.JsoImplementor::m()();
+  }-*/;
+
+  public native JsoImplementor newImplementor(int n) /*-{
+    return {a:n};
+  }-*/;
+
+  public void testDevirtualization() {
+    assertEquals(2, doubleToInt(new Double(2.2)));
+    assertEquals(2, numberToInt(new Double(2.2)));
+    assertEquals(1, callMOnDual(new JavaImplementor()));
+    assertEquals(2, callMOnDual(newImplementor(2)));
+    assertEquals(3, callMOnJso(newImplementor(3)));
+  }
 }