Commit SingleJsoImpl branch to trunk.

Patch by: bobv
Review by: spoon, scottb


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@4689 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/typeinfo/JRealClassType.java b/dev/core/src/com/google/gwt/core/ext/typeinfo/JRealClassType.java
index e326756..201394f 100644
--- a/dev/core/src/com/google/gwt/core/ext/typeinfo/JRealClassType.java
+++ b/dev/core/src/com/google/gwt/core/ext/typeinfo/JRealClassType.java
@@ -15,6 +15,8 @@
  */
 package com.google.gwt.core.ext.typeinfo;
 
+import com.google.gwt.core.client.SingleJsoImpl;
+
 import java.lang.annotation.Annotation;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -93,6 +95,12 @@
   public void addAnnotations(
       Map<Class<? extends Annotation>, Annotation> declaredAnnotations) {
     annotations.addAnnotations(declaredAnnotations);
+    for (Class<? extends Annotation> clazz : declaredAnnotations.keySet()) {
+      if (SingleJsoImpl.class.equals(clazz)) {
+        oracle.addSingleJsoInterface(this);
+        break;
+      }
+    }
   }
 
   public void addImplementedInterface(JClassType intf) {
@@ -286,7 +294,7 @@
    * Determines if the class can be constructed using a simple <code>new</code>
    * operation. Specifically, the class must
    * <ul>
-   * <li>be a class rather than an interface, </li>
+   * <li>be a class rather than an interface,</li>
    * <li>have either no constructors or a parameterless constructor, and</li>
    * <li>be a top-level class or a static nested class.</li>
    * </ul>
diff --git a/dev/core/src/com/google/gwt/core/ext/typeinfo/TypeOracle.java b/dev/core/src/com/google/gwt/core/ext/typeinfo/TypeOracle.java
index 424a3ca..cda9804 100644
--- a/dev/core/src/com/google/gwt/core/ext/typeinfo/TypeOracle.java
+++ b/dev/core/src/com/google/gwt/core/ext/typeinfo/TypeOracle.java
@@ -23,6 +23,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -198,6 +199,8 @@
 
   private final Map<String, List<JWildcardType>> wildcardTypes = new HashMap<String, List<JWildcardType>>();
 
+  private final Set<JRealClassType> singleJsoImplTypes = new HashSet<JRealClassType>();
+
   public TypeOracle() {
     // Always create the default package.
     //
@@ -216,10 +219,10 @@
 
   /**
    * Finds a class or interface given its fully-qualified name.
-   *
-   * @param name fully-qualified class/interface name -  for nested
-   * classes, use its source name rather than its binary name (that is, use a
-   * "." rather than a "$")
+   * 
+   * @param name fully-qualified class/interface name - for nested classes, use
+   *          its source name rather than its binary name (that is, use a "."
+   *          rather than a "$")
    * 
    * @return <code>null</code> if the type is not found
    */
@@ -434,6 +437,14 @@
   }
 
   /**
+   * Returns an unmodifiable, live view of all interface types annotated with
+   * the SingleJsoImpl annotation.
+   */
+  public Set<JRealClassType> getSingleJsoImplTypes() {
+    return Collections.unmodifiableSet(singleJsoImplTypes);
+  }
+
+  /**
    * Finds a type given its fully qualified name. For nested classes, use its
    * source name rather than its binary name (that is, use a "." rather than a
    * "$").
@@ -605,6 +616,12 @@
     recentTypes.add(newType);
   }
 
+  void addSingleJsoInterface(JRealClassType singleJsoImplType) {
+    assert singleJsoImplType.isInterface() == singleJsoImplType : singleJsoImplType.getName()
+        + " has SingleJsoImpl, but is not an interface";
+    singleJsoImplTypes.add(singleJsoImplType);
+  }
+
   void invalidate(JRealClassType realClassType) {
     invalidatedTypes.add(realClassType);
   }
@@ -983,7 +1000,7 @@
       JRealClassType classType = iter.next();
       String fqcn = classType.getQualifiedSourceName();
       allTypes.remove(fqcn);
-
+      singleJsoImplTypes.remove(classType);
       JPackage pkg = classType.getPackage();
       if (pkg != null) {
         pkg.remove(classType);
diff --git a/dev/core/src/com/google/gwt/dev/javac/CompilationState.java b/dev/core/src/com/google/gwt/dev/javac/CompilationState.java
index 3ada290..5abfa26 100644
--- a/dev/core/src/com/google/gwt/dev/javac/CompilationState.java
+++ b/dev/core/src/com/google/gwt/dev/javac/CompilationState.java
@@ -82,6 +82,8 @@
    */
   private final JavaSourceOracle sourceOracle;
 
+  private CompilationUnitInvalidator.InvalidatorState invalidatorState = new CompilationUnitInvalidator.InvalidatorState();
+
   /**
    * Construct a new {@link CompilationState}.
    * 
@@ -170,6 +172,19 @@
     CompilationUnitInvalidator.invalidateUnitsWithInvalidRefs(TreeLogger.NULL,
         getCompilationUnits());
 
+    /*
+     * Only retain state for units marked as CHECKED; because CHECKED units
+     * won't be revalidated.
+     */
+    Set<CompilationUnit> toRetain = new HashSet<CompilationUnit>(exposedUnits);
+    for (Iterator<CompilationUnit> it = toRetain.iterator(); it.hasNext();) {
+      CompilationUnit unit = it.next();
+      if (unit.getState() != State.CHECKED) {
+        it.remove();
+      }
+    }
+    invalidatorState.retainAll(toRetain);
+
     jdtCompiler = new JdtCompiler();
     compile(logger, getCompilationUnits());
     mediator.refresh(logger, getCompilationUnits());
@@ -188,8 +203,8 @@
           logger, newUnits);
 
       // Check all units using our custom checks.
-      CompilationUnitInvalidator.validateCompilationUnits(newUnits,
-          jdtCompiler.getBinaryTypeNames());
+      CompilationUnitInvalidator.validateCompilationUnits(invalidatorState,
+          newUnits, jdtCompiler.getBinaryTypeNames());
 
       // More units may have errors now.
       anyErrors |= CompilationUnitInvalidator.invalidateUnitsWithErrors(logger,
diff --git a/dev/core/src/com/google/gwt/dev/javac/CompilationUnitInvalidator.java b/dev/core/src/com/google/gwt/dev/javac/CompilationUnitInvalidator.java
index 9ff8499..e5afa2a 100644
--- a/dev/core/src/com/google/gwt/dev/javac/CompilationUnitInvalidator.java
+++ b/dev/core/src/com/google/gwt/dev/javac/CompilationUnitInvalidator.java
@@ -24,6 +24,7 @@
 import org.eclipse.jdt.internal.compiler.CompilationResult;
 import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
 
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Set;
@@ -32,12 +33,23 @@
  * Helper class to invalidate units in a set based on errors or references to
  * other invalidate units.
  * 
- * TODO: {@link ClassFileReader#hasStructuralChanges(byte[])} could help us
- * optimize this process!
+ * TODO: ClassFileReader#hasStructuralChanges(byte[]) could help us optimize
+ * this process!
  */
 public class CompilationUnitInvalidator {
 
   /**
+   * Maintain cross-validation state.
+   */
+  public static class InvalidatorState {
+    private final JSORestrictionsChecker.CheckerState jsoState = new JSORestrictionsChecker.CheckerState();
+
+    public void retainAll(Collection<CompilationUnit> toRetain) {
+      jsoState.retainAll(toRetain);
+    }
+  }
+
+  /**
    * For all units containing one or more errors whose state is currently
    * {@link State#COMPILED}, each unit's error(s) will be logged to
    * <code>logger</code> and each unit's state will be set to
@@ -129,16 +141,17 @@
     } while (changed);
   }
 
-  public static void validateCompilationUnits(Set<CompilationUnit> units,
-      Set<String> validBinaryTypeNames) {
+  public static void validateCompilationUnits(InvalidatorState state,
+      Set<CompilationUnit> units, Set<String> validBinaryTypeNames) {
     for (CompilationUnit unit : units) {
       if (unit.getState() == State.COMPILED) {
         CompilationUnitDeclaration jdtCud = unit.getJdtCud();
-        JSORestrictionsChecker.check(jdtCud);
+        JSORestrictionsChecker.check(state.jsoState, jdtCud);
         LongFromJSNIChecker.check(jdtCud);
         BinaryTypeReferenceRestrictionsChecker.check(jdtCud,
             validBinaryTypeNames);
       }
     }
+    state.jsoState.finalCheck();
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/javac/JSORestrictionsChecker.java b/dev/core/src/com/google/gwt/dev/javac/JSORestrictionsChecker.java
index b6c7390..e1c3dff 100644
--- a/dev/core/src/com/google/gwt/dev/javac/JSORestrictionsChecker.java
+++ b/dev/core/src/com/google/gwt/dev/javac/JSORestrictionsChecker.java
@@ -15,11 +15,14 @@
  */
 package com.google.gwt.dev.javac;
 
+import com.google.gwt.core.client.SingleJsoImpl;
 import com.google.gwt.dev.util.InstalledHelpInfo;
 
+import org.eclipse.jdt.core.compiler.CharOperation;
 import org.eclipse.jdt.internal.compiler.ASTVisitor;
 import org.eclipse.jdt.internal.compiler.ast.ASTNode;
 import org.eclipse.jdt.internal.compiler.ast.AllocationExpression;
+import org.eclipse.jdt.internal.compiler.ast.Annotation;
 import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
 import org.eclipse.jdt.internal.compiler.ast.ConstructorDeclaration;
 import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
@@ -33,32 +36,263 @@
 import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
 import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
 
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
 import java.util.Stack;
 
 /**
  * Check a compilation unit for violations of
  * {@link com.google.gwt.core.client.JavaScriptObject JavaScriptObject} (JSO)
- * restrictions. The restrictions are:
+ * restrictions. The restrictions are summarized in
+ * <code>jsoRestrictions.html</code>.
  * 
- * <ul>
- * <li> All instance methods on JSO classes must be one of: final, private, or a
- * member of a final class.
- * <li> JSO classes cannot implement interfaces that define methods.
- * <li> No instance methods on JSO classes may override another method. (This
- * catches accidents where JSO itself did not finalize some method from its
- * superclass.)
- * <li> JSO classes cannot have instance fields.
- * <li> "new" operations cannot be used with JSO classes.
- * <li> Every JSO class must have precisely one constructor, and it must be
- * protected, empty, and no-argument.
- * <li> Nested JSO classes must be static.
- * </ul>
  * 
  * Any violations found are attached as errors on the
  * CompilationUnitDeclaration.
+ * 
+ * @see <a
+ *      href="http://code.google.com/p/google-web-toolkit/wiki/OverlayTypes">Overlay
+ *      types design doc</a>
+ * @see jsoRestrictions.html
  */
 public class JSORestrictionsChecker {
 
+  /**
+   * The order in which the checker will process types is undefined, so this
+   * type accumulates the information necessary for sanity-checking the JSO
+   * types.
+   */
+  public static class CheckerState {
+
+    /**
+     * This maps interfaces tagged with the SingleJsoImpl annotation to the
+     * names of their declared super-interfaces.
+     */
+    private final Map<TypeDeclaration, Set<String>> interfacesToSuperInterfaces = new HashMap<TypeDeclaration, Set<String>>();
+    private final Map<TypeDeclaration, Set<String>> trivialToSuperInterfaces = new HashMap<TypeDeclaration, Set<String>>();
+
+    /**
+     * This maps JSO implementation types to their implemented SingleJsoImpl
+     * interfaces.
+     */
+    private final Map<TypeDeclaration, Set<String>> jsoImplsToInterfaces = new HashMap<TypeDeclaration, Set<String>>();
+
+    /**
+     * Used for error reporting.
+     */
+    private final Map<TypeDeclaration, CompilationUnitDeclaration> nodesToCuds = new IdentityHashMap<TypeDeclaration, CompilationUnitDeclaration>();
+
+    /**
+     * A convenience for validity checking.
+     */
+    private final Set<String> singleJsoImplInterfaceNames = new HashSet<String>();
+    private final Set<String> trivialInterfaceNames = new HashSet<String>();
+
+    /**
+     * Just used for sanity checking. Not populated unless assertions are on.
+     */
+    private final Set<String> regularInterfaceNames = getClass().desiredAssertionStatus()
+        ? new HashSet<String>() : null;
+
+    /**
+     * This method should be called after all CUDs are passed into check().
+     */
+    public void finalCheck() {
+      /*
+       * Compute all interfaces that are completely trivial.
+       */
+      Set<String> tagInterfaceNames = new HashSet<String>();
+      for (Map.Entry<TypeDeclaration, Set<String>> entry : trivialToSuperInterfaces.entrySet()) {
+        if (entry.getValue().isEmpty()
+            || trivialInterfaceNames.containsAll(entry.getValue())) {
+          tagInterfaceNames.add(CharOperation.toString(entry.getKey().binding.compoundName));
+        }
+      }
+
+      /*
+       * Make sure that all SingleJsoImpl interfaces extends only SingleJsoImpl
+       * interfaces or tag interfaces.
+       */
+      for (Map.Entry<TypeDeclaration, Set<String>> entry : interfacesToSuperInterfaces.entrySet()) {
+        TypeDeclaration node = entry.getKey();
+
+        for (String intfName : entry.getValue()) {
+          if (tagInterfaceNames.contains(intfName)
+              || singleJsoImplInterfaceNames.contains(intfName)) {
+            continue;
+          } else {
+            assert regularInterfaceNames.contains(intfName);
+            errorOn(node, nodesToCuds.get(node), ERR_INTF_EXTENDS_BAD_INTF);
+          }
+        }
+      }
+
+      /*
+       * Make sure that any interfaces implemented by the JSO types have the
+       * SingleJsoImpl annotation. Also, check the one-and-only JSO
+       * implementation of the interface types.
+       */
+      Map<String, TypeDeclaration> singleImplementations = new HashMap<String, TypeDeclaration>();
+      for (Map.Entry<TypeDeclaration, Set<String>> entry : jsoImplsToInterfaces.entrySet()) {
+        TypeDeclaration node = entry.getKey();
+        for (String intfName : entry.getValue()) {
+          if (!singleJsoImplInterfaceNames.contains(intfName)) {
+            errorOn(node, nodesToCuds.get(node),
+                errInterfaceWithMethodsNoAnnotation(intfName));
+          }
+
+          if (!singleImplementations.containsKey(intfName)) {
+            singleImplementations.put(intfName, node);
+          } else {
+            /*
+             * Emit an error if the previously-defined type is neither a
+             * supertype nor subtype of the current type
+             */
+            TypeDeclaration previous = singleImplementations.get(intfName);
+            if (!(hasSupertypeNamed(node, previous.binding.compoundName) || hasSupertypeNamed(
+                previous, node.binding.compoundName))) {
+              String nodeName = CharOperation.toString(node.binding.compoundName);
+              String previousName = CharOperation.toString(previous.binding.compoundName);
+
+              // Provide consistent reporting, regardless of visitation order
+              if (nodeName.compareTo(previousName) < 0) {
+                String msg = errAlreadyImplemented(intfName, nodeName,
+                    previousName);
+                errorOn(node, nodesToCuds.get(node), msg);
+                errorOn(previous, nodesToCuds.get(previous), msg);
+              } else {
+                String msg = errAlreadyImplemented(intfName, previousName,
+                    nodeName);
+                errorOn(previous, nodesToCuds.get(previous), msg);
+                errorOn(node, nodesToCuds.get(node), msg);
+              }
+            }
+          }
+        }
+      }
+    }
+
+    public void retainAll(Collection<CompilationUnit> units) {
+      // Fast-path for removing everything
+      if (units.isEmpty()) {
+        interfacesToSuperInterfaces.clear();
+        trivialToSuperInterfaces.clear();
+        jsoImplsToInterfaces.clear();
+        nodesToCuds.clear();
+        singleJsoImplInterfaceNames.clear();
+        trivialInterfaceNames.clear();
+        if (regularInterfaceNames != null) {
+          regularInterfaceNames.clear();
+        }
+        return;
+      }
+
+      // Build up a list of the types that should be retained
+      Set<String> retainedTypeNames = new HashSet<String>();
+
+      for (CompilationUnit u : units) {
+        for (CompiledClass c : u.getCompiledClasses()) {
+          // Can't rely on getJdtCud() because those are pruned
+          retainedTypeNames.add(c.getSourceName());
+        }
+      }
+
+      // Loop over all TypeDeclarations that we have
+      for (Iterator<TypeDeclaration> it = nodesToCuds.keySet().iterator(); it.hasNext();) {
+        TypeDeclaration decl = it.next();
+
+        // Remove the TypeDeclaration if it's not in the list of retained types
+        if (!retainedTypeNames.contains(CharOperation.toString(decl.binding.compoundName))) {
+          it.remove();
+
+          assert interfacesToSuperInterfaces.containsKey(decl)
+              || trivialToSuperInterfaces.containsKey(decl)
+              || jsoImplsToInterfaces.containsKey(decl) : "TypeDeclaration "
+              + CharOperation.toString(decl.binding.compoundName)
+              + " in nodesToCuds, but not in any of the maps";
+
+          interfacesToSuperInterfaces.remove(decl);
+          trivialToSuperInterfaces.remove(decl);
+          jsoImplsToInterfaces.remove(decl);
+        }
+      }
+
+      singleJsoImplInterfaceNames.retainAll(retainedTypeNames);
+      trivialInterfaceNames.retainAll(retainedTypeNames);
+      if (regularInterfaceNames != null) {
+        regularInterfaceNames.retainAll(retainedTypeNames);
+      }
+    }
+
+    private void add(Map<TypeDeclaration, Set<String>> map,
+        TypeDeclaration key, String value) {
+      Set<String> set = map.get(key);
+      if (set == null) {
+        map.put(key, set = new HashSet<String>());
+      }
+      set.add(value);
+    }
+
+    private void addJsoInterface(TypeDeclaration jsoType,
+        CompilationUnitDeclaration cud, String interfaceName) {
+      nodesToCuds.put(jsoType, cud);
+      add(jsoImplsToInterfaces, jsoType, interfaceName);
+    }
+
+    private void empty(Map<TypeDeclaration, Set<String>> map,
+        TypeDeclaration key) {
+      assert !map.containsKey(key);
+      map.put(key, Collections.<String> emptySet());
+    }
+
+    private boolean hasSupertypeNamed(TypeDeclaration type, char[][] qType) {
+      ReferenceBinding b = type.binding;
+      while (b != null) {
+        if (CharOperation.equals(b.compoundName, qType)) {
+          return true;
+        }
+        b = b.superclass();
+      }
+      return false;
+    }
+
+    private void jsoSingleImplInterface(TypeDeclaration type,
+        CompilationUnitDeclaration cud, String superInterface) {
+      nodesToCuds.put(type, cud);
+      singleJsoImplInterfaceNames.add(CharOperation.toString(type.binding.compoundName));
+      if (superInterface == null) {
+        empty(interfacesToSuperInterfaces, type);
+      } else {
+        add(interfacesToSuperInterfaces, type, superInterface);
+      }
+    }
+
+    private void regularInterface(TypeDeclaration intfType,
+        CompilationUnitDeclaration cud, String superInterfaceName) {
+      // Just used for sanity checking
+      if (getClass().desiredAssertionStatus()) {
+        regularInterfaceNames.add(CharOperation.toString(intfType.binding.compoundName));
+      }
+    }
+
+    private void trivialInterface(TypeDeclaration intfType,
+        CompilationUnitDeclaration cud, String superInterfaceName) {
+      nodesToCuds.put(intfType, cud);
+      trivialInterfaceNames.add(CharOperation.toString(intfType.binding.compoundName));
+      if (superInterfaceName == null) {
+        empty(trivialToSuperInterfaces, intfType);
+      } else {
+        add(trivialToSuperInterfaces, intfType, superInterfaceName);
+      }
+    }
+  }
+
   private class JSORestrictionsVisitor extends ASTVisitor implements
       ClassFileConstants {
 
@@ -153,6 +387,61 @@
     }
 
     private boolean checkType(TypeDeclaration type) {
+      /*
+       * See if we're looking at something tagged with a SingleJsoImpl
+       * annotation.
+       */
+      if (type.annotations != null) {
+        for (Annotation a : type.annotations) {
+          if (!(a.resolvedType instanceof ReferenceBinding)) {
+            continue;
+          }
+
+          ReferenceBinding refBinding = (ReferenceBinding) a.resolvedType;
+          if (!SINGLE_JSO_IMPL_CLASS.equals(new String(
+              refBinding.readableName()))) {
+            continue;
+          }
+
+          if (!type.binding.isInterface()) {
+            errorOn(type, ERR_ONLY_INTERFACES);
+            continue;
+          }
+
+          // Interfaces should not have superclasses, just super-interfaces
+          assert type.superclass == null;
+
+          if (type.superInterfaces != null) {
+            for (ReferenceBinding ref : type.binding.superInterfaces) {
+              String superInterfaceName = CharOperation.toString(ref.compoundName);
+              state.jsoSingleImplInterface(type, cud, superInterfaceName);
+            }
+          } else {
+            state.jsoSingleImplInterface(type, cud, null);
+          }
+        }
+      }
+
+      if (type.binding.isInterface()) {
+        boolean isTrivial = type.methods == null || type.methods.length == 0;
+        if (type.superInterfaces != null) {
+          for (ReferenceBinding ref : type.binding.superInterfaces) {
+            String superInterfaceName = CharOperation.toString(ref.compoundName);
+            if (isTrivial) {
+              state.trivialInterface(type, cud, superInterfaceName);
+            } else {
+              state.regularInterface(type, cud, superInterfaceName);
+            }
+          }
+        } else {
+          if (isTrivial) {
+            state.trivialInterface(type, cud, null);
+          } else {
+            state.regularInterface(type, cud, null);
+          }
+        }
+      }
+
       if (!isJsoSubclass(type.binding)) {
         return false;
       }
@@ -163,12 +452,17 @@
       ReferenceBinding[] interfaces = type.binding.superInterfaces();
       if (interfaces != null) {
         for (ReferenceBinding interf : interfaces) {
-          if (interf.methods() != null && interf.methods().length > 0) {
-            String intfName = String.copyValueOf(interf.shortReadableName());
-            errorOn(type, errInterfaceWithMethods(intfName));
+          if (interf.methods() == null) {
+            continue;
+          }
+
+          if (interf.methods().length > 0) {
+            String intfName = CharOperation.toString(interf.compoundName);
+            state.addJsoInterface(type, cud, intfName);
           }
         }
       }
+
       return true;
     }
 
@@ -188,32 +482,52 @@
   static final String ERR_CONSTRUCTOR_WITH_PARAMETERS = "Constructors must not have parameters in subclasses of JavaScriptObject";
   static final String ERR_INSTANCE_FIELD = "Instance fields cannot be used in subclasses of JavaScriptObject";
   static final String ERR_INSTANCE_METHOD_NONFINAL = "Instance methods must be 'final' in non-final subclasses of JavaScriptObject";
+  static final String ERR_INTF_EXTENDS_BAD_INTF = "SingleJsoImpl interfaces must only extend other SingleJsoImpl interfaces or tag interfaces";
   static final String ERR_IS_NONSTATIC_NESTED = "Nested classes must be 'static' if they extend JavaScriptObject";
   static final String ERR_NEW_JSO = "'new' cannot be used to create instances of JavaScriptObject subclasses; instances must originate in JavaScript";
   static final String ERR_NONEMPTY_CONSTRUCTOR = "Constructors must be totally empty in subclasses of JavaScriptObject";
   static final String ERR_NONPROTECTED_CONSTRUCTOR = "Constructors must be 'protected' in subclasses of JavaScriptObject";
+  static final String ERR_ONLY_INTERFACES = "SingleJsoImpl may only be applied to interface types";
   static final String ERR_OVERRIDDEN_METHOD = "Methods cannot be overridden in JavaScriptObject subclasses";
   static final String JSO_CLASS = "com/google/gwt/core/client/JavaScriptObject";
+  static final String SINGLE_JSO_IMPL_CLASS = SingleJsoImpl.class.getName();
 
   /**
    * Checks an entire
    * {@link org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration}.
    * 
    */
-  public static void check(CompilationUnitDeclaration cud) {
-    JSORestrictionsChecker checker = new JSORestrictionsChecker(cud);
+  public static void check(CheckerState state, CompilationUnitDeclaration cud) {
+    JSORestrictionsChecker checker = new JSORestrictionsChecker(state, cud);
     checker.check();
   }
 
-  static String errInterfaceWithMethods(String intfName) {
+  static String errAlreadyImplemented(String intfName, String impl1,
+      String impl2) {
+    return "Only one JavaScriptObject type may implement the methods of a "
+        + "@SingleJsoImpl type interface. The interface (" + intfName
+        + ") is implemented by both (" + impl1 + ") and (" + impl2 + ")";
+  }
+
+  static String errInterfaceWithMethodsNoAnnotation(String intfName) {
     return "JavaScriptObject classes cannot implement interfaces with methods ("
-        + intfName + ")";
+        + intfName + ") that do not define the @SingleJsoImpl annotation";
+  }
+
+  private static void errorOn(ASTNode node, CompilationUnitDeclaration cud,
+      String error) {
+    GWTProblem.recordInCud(node, cud, error, new InstalledHelpInfo(
+        "jsoRestrictions.html"));
   }
 
   private final CompilationUnitDeclaration cud;
 
-  private JSORestrictionsChecker(CompilationUnitDeclaration cud) {
+  private final CheckerState state;
+
+  private JSORestrictionsChecker(CheckerState state,
+      CompilationUnitDeclaration cud) {
     this.cud = cud;
+    this.state = state;
   }
 
   private void check() {
@@ -221,8 +535,7 @@
   }
 
   private void errorOn(ASTNode node, String error) {
-    GWTProblem.recordInCud(node, cud, error, new InstalledHelpInfo(
-        "jsoRestrictions.html"));
+    errorOn(node, cud, error);
   }
 
   private boolean isJsoSubclass(TypeBinding typeBinding) {
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
index 1dd2a9a..ff003f3 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -174,7 +174,7 @@
       Pruner.exec(jprogram, false);
 
       // (7) Generate a JavaScript code DOM from the Java type declarations
-      jprogram.typeOracle.recomputeClinits();
+      jprogram.typeOracle.recomputeAfterOptimizations();
       final JavaToJavaScriptMap map = GenerateJavaScriptAST.exec(jprogram,
           jsProgram, options.getOutput());
 
@@ -420,7 +420,7 @@
       }
 
       // Recompute clinits each time, they can become empty.
-      jprogram.typeOracle.recomputeClinits();
+      jprogram.typeOracle.recomputeAfterOptimizations();
       didChange = false;
 
       // Remove unreferenced types, fields, methods, [params, locals]
@@ -457,7 +457,7 @@
        * Ensure that references to dead clinits are removed. Otherwise, the
        * application won't run reliably.
        */
-      jprogram.typeOracle.recomputeClinits();
+      jprogram.typeOracle.recomputeAfterOptimizations();
       DeadCodeElimination.exec(jprogram);
     }
   }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java
index 508b62b..c23140d 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java
@@ -19,9 +19,11 @@
 
 import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -203,6 +205,8 @@
 
   private final Map<JClassType, Set<JInterfaceType>> couldImplementMap = new IdentityHashMap<JClassType, Set<JInterfaceType>>();
 
+  private final Set<JInterfaceType> dualImpl = new HashSet<JInterfaceType>();
+
   private final Set<JReferenceType> hasClinitSet = new HashSet<JReferenceType>();
 
   private final Map<JClassType, Set<JInterfaceType>> implementsMap = new IdentityHashMap<JClassType, Set<JInterfaceType>>();
@@ -213,6 +217,8 @@
 
   private JClassType javaLangObject = null;
 
+  private final Map<JInterfaceType, JClassType> jsoSingleImpls = new IdentityHashMap<JInterfaceType, JClassType>();
+
   private final JProgram program;
 
   private final Map<JClassType, Set<JClassType>> subClassMap = new IdentityHashMap<JClassType, Set<JClassType>>();
@@ -229,6 +235,29 @@
     this.program = program;
   }
 
+  /**
+   * Collect all supertypes and superinterfaces for a type.
+   */
+  public Set<JReferenceType> allAssignableFrom(JReferenceType type) {
+    Set<JReferenceType> toReturn = new HashSet<JReferenceType>();
+    List<JReferenceType> q = new LinkedList<JReferenceType>();
+    q.add(type);
+
+    while (!q.isEmpty()) {
+      JReferenceType t = q.remove(0);
+
+      if (toReturn.add(t)) {
+        if (t.extnds != null) {
+          q.add(t.extnds);
+        }
+
+        q.addAll(t.implments);
+      }
+    }
+
+    return toReturn;
+  }
+
   public boolean canTheoreticallyCast(JReferenceType type, JReferenceType qType) {
     JClassType jlo = program.getTypeJavaLangObject();
     if (type == qType || type == jlo) {
@@ -409,6 +438,8 @@
         computeVirtualUpRefs((JClassType) type);
       }
     }
+
+    computeSingleJsoImplData();
   }
 
   /**
@@ -418,6 +449,18 @@
     return getOrCreate(superInterfaceMap, type).contains(qType);
   }
 
+  public JMethod findConcreteImplementation(JMethod method,
+      JClassType concreteType) {
+    for (JMethod m : concreteType.methods) {
+      if (getAllOverrides(m).contains(method)) {
+        if (!m.isAbstract()) {
+          return m;
+        }
+      }
+    }
+    return null;
+  }
+
   public Set<JMethod> getAllOverrides(JMethod method) {
     return getAllOverrides(method, instantiatedTypes);
   }
@@ -457,6 +500,14 @@
     return results;
   }
 
+  public Set<JInterfaceType> getInterfacesWithJavaAndJsoImpls() {
+    return Collections.unmodifiableSet(dualImpl);
+  }
+
+  public Map<JInterfaceType, JClassType> getSingleJsoImpls() {
+    return Collections.unmodifiableMap(jsoSingleImpls);
+  }
+
   public boolean hasClinit(JReferenceType type) {
     return hasClinitSet.contains(type);
   }
@@ -487,13 +538,19 @@
     return getOrCreate(superClassMap, type).contains(qType);
   }
 
-  public void recomputeClinits() {
+  /**
+   * This method should be called after altering the types that are live in the
+   * associated JProgram.
+   */
+  public void recomputeAfterOptimizations() {
     hasClinitSet.clear();
     Set<JReferenceType> computed = new HashSet<JReferenceType>();
     for (int i = 0; i < program.getDeclaredTypes().size(); ++i) {
       JReferenceType type = program.getDeclaredTypes().get(i);
       computeHasClinit(type, computed);
     }
+
+    computeSingleJsoImplData();
   }
 
   public void setInstantiatedTypes(Set<JReferenceType> instantiatedTypes) {
@@ -591,6 +648,52 @@
     }
   }
 
+  private void computeSingleJsoImplData() {
+    dualImpl.clear();
+    jsoSingleImpls.clear();
+
+    for (JReferenceType type : program.getDeclaredTypes()) {
+      if (!program.isJavaScriptObject(type)) {
+        if (type instanceof JClassType) {
+          dualImpl.addAll(type.implments);
+        }
+        continue;
+      }
+
+      for (JReferenceType refType : allAssignableFrom(type)) {
+        if (!(refType instanceof JInterfaceType)) {
+          continue;
+        }
+        JInterfaceType intr = (JInterfaceType) refType;
+        if (intr.methods.size() <= 1) {
+          // Ignore tag interfaces
+          assert intr.methods.size() == 0
+              || intr.methods.get(0).getName().equals("$clinit");
+          continue;
+        }
+
+        if (jsoSingleImpls.containsKey(intr)) {
+          // See if we're looking at a supertype
+          JClassType alreadySeen = jsoSingleImpls.get(intr);
+
+          if (allAssignableFrom(alreadySeen).contains(type)) {
+            jsoSingleImpls.put(intr, (JClassType) type);
+          } else {
+            assert allAssignableFrom(type).contains(alreadySeen) : "Already recorded "
+                + alreadySeen.getName()
+                + " as single impl for "
+                + intr.getName()
+                + " while looking at unrelated type "
+                + type.getName();
+          }
+        } else {
+          jsoSingleImpls.put(intr, (JClassType) type);
+        }
+      }
+    }
+    dualImpl.retainAll(jsoSingleImpls.keySet());
+  }
+
   /**
    * WEIRD: Suppose class Foo declares void f(){} and unrelated interface I also
    * declares void f(). Then suppose Bar extends Foo implements I and doesn't
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/CastNormalizer.java b/dev/core/src/com/google/gwt/dev/jjs/impl/CastNormalizer.java
index b1f061c..7fd3680 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/CastNormalizer.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/CastNormalizer.java
@@ -27,6 +27,7 @@
 import com.google.gwt.dev.jjs.ast.JExpression;
 import com.google.gwt.dev.jjs.ast.JInstanceOf;
 import com.google.gwt.dev.jjs.ast.JIntLiteral;
+import com.google.gwt.dev.jjs.ast.JInterfaceType;
 import com.google.gwt.dev.jjs.ast.JMethod;
 import com.google.gwt.dev.jjs.ast.JMethodCall;
 import com.google.gwt.dev.jjs.ast.JModVisitor;
@@ -412,6 +413,12 @@
    */
   private class ReplaceTypeChecksVisitor extends JModVisitor {
 
+    private final Set<JInterfaceType> dualImpls;
+
+    public ReplaceTypeChecksVisitor(JProgram program) {
+      dualImpls = program.typeOracle.getInterfacesWithJavaAndJsoImpls();
+    }
+
     @Override
     public void endVisit(JCastOperation x, Context ctx) {
       JExpression replaceExpr;
@@ -441,9 +448,19 @@
           // just remove the cast
           replaceExpr = curExpr;
         } else {
+
+          JMethod method;
           boolean isJsoCast = program.isJavaScriptObject(toType);
-          JMethod method = program.getIndexedMethod(isJsoCast
-              ? "Cast.dynamicCastJso" : "Cast.dynamicCast");
+          if (isJsoCast) {
+            // A cast to JSO
+            method = program.getIndexedMethod("Cast.dynamicCastJso");
+          } else if (dualImpls.contains(toType)) {
+            // A cast that may succeed when the object is a JSO
+            method = program.getIndexedMethod("Cast.dynamicCastAllowJso");
+          } else {
+            // A regular cast
+            method = program.getIndexedMethod("Cast.dynamicCast");
+          }
           // override the type of the called method with the target cast type
           JMethodCall call = new JMethodCall(program, x.getSourceInfo(), null,
               method, toType);
@@ -548,9 +565,15 @@
             x.getExpr(), nullLit);
         ctx.replaceMe(eq);
       } else {
+        JMethod method;
         boolean isJsoCast = program.isJavaScriptObject(toType);
-        JMethod method = program.getIndexedMethod(isJsoCast
-            ? "Cast.instanceOfJso" : "Cast.instanceOf");
+        if (isJsoCast) {
+          method = program.getIndexedMethod("Cast.instanceOfJso");
+        } else if (dualImpls.contains(toType)) {
+          method = program.getIndexedMethod("Cast.instanceOfOrJso");
+        } else {
+          method = program.getIndexedMethod("Cast.instanceOf");
+        }
         JMethodCall call = new JMethodCall(program, x.getSourceInfo(), null,
             method);
         call.getArgs().add(x.getExpr());
@@ -590,7 +613,7 @@
       assigner.computeTypeIds();
     }
     {
-      ReplaceTypeChecksVisitor replacer = new ReplaceTypeChecksVisitor();
+      ReplaceTypeChecksVisitor replacer = new ReplaceTypeChecksVisitor(program);
       replacer.accept(program);
     }
   }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java
index afda14a..466eb00 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java
@@ -2943,7 +2943,15 @@
           JMethod method, JsContext<JsExpression> ctx) {
         JReferenceType enclosingType = method.getEnclosingType();
         if (enclosingType != null) {
-          if (method.isStatic() && nameRef.getQualifier() != null) {
+          JClassType jsoImplType = program.typeOracle.getSingleJsoImpls().get(
+              enclosingType);
+          if (jsoImplType != null) {
+            reportJsniError(info, methodDecl, "Illegal reference to method '"
+                + method.getName() + "' in type '" + enclosingType.getName()
+                + "', which is implemented by an overlay type '"
+                + jsoImplType.getName() + "'. Use a stronger type in the JSNI "
+                + "identifier or a Java trampoline method.");
+          } else if (method.isStatic() && nameRef.getQualifier() != null) {
             reportJsniError(info, methodDecl,
                 "Cannot make a qualified reference to the static method "
                     + method.getName());
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JavaScriptObjectNormalizer.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JavaScriptObjectNormalizer.java
index 5fc544d..56fe60a 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JavaScriptObjectNormalizer.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JavaScriptObjectNormalizer.java
@@ -15,31 +15,59 @@
  */
 package com.google.gwt.dev.jjs.impl;
 
+import com.google.gwt.dev.jjs.SourceInfo;
 import com.google.gwt.dev.jjs.ast.Context;
 import com.google.gwt.dev.jjs.ast.JArrayType;
 import com.google.gwt.dev.jjs.ast.JCastOperation;
 import com.google.gwt.dev.jjs.ast.JClassLiteral;
+import com.google.gwt.dev.jjs.ast.JClassType;
+import com.google.gwt.dev.jjs.ast.JConditional;
+import com.google.gwt.dev.jjs.ast.JExpression;
 import com.google.gwt.dev.jjs.ast.JField;
 import com.google.gwt.dev.jjs.ast.JInstanceOf;
+import com.google.gwt.dev.jjs.ast.JInterfaceType;
 import com.google.gwt.dev.jjs.ast.JLocal;
+import com.google.gwt.dev.jjs.ast.JLocalRef;
 import com.google.gwt.dev.jjs.ast.JMethod;
+import com.google.gwt.dev.jjs.ast.JMethodBody;
+import com.google.gwt.dev.jjs.ast.JMethodCall;
 import com.google.gwt.dev.jjs.ast.JModVisitor;
 import com.google.gwt.dev.jjs.ast.JNewArray;
 import com.google.gwt.dev.jjs.ast.JParameter;
 import com.google.gwt.dev.jjs.ast.JProgram;
 import com.google.gwt.dev.jjs.ast.JReferenceType;
 import com.google.gwt.dev.jjs.ast.JType;
+import com.google.gwt.dev.jjs.ast.JVisitor;
+import com.google.gwt.dev.jjs.ast.js.JMultiExpression;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
 
 /**
  * Replace references to JSO subtypes with JSO itself.
  */
 public class JavaScriptObjectNormalizer {
+  private class Collector extends JVisitor {
 
+    @Override
+    public void endVisit(JClassType x, Context ctx) {
+      if (program.isJavaScriptObject(x)) {
+        for (JInterfaceType intr : x.implments) {
+          if (isTagInterface(intr) && !jsoType.implments.contains(intr)) {
+            jsoType.implments.add(intr);
+          }
+        }
+      }
+    }
+  }
   /**
    * Map types from JSO subtypes to JSO itself.
    */
   private class NormalizeVisitor extends JModVisitor {
 
+    private final Stack<JMethodBody> currentMethodBody = new Stack<JMethodBody>();
+
     @Override
     public void endVisit(JCastOperation x, Context ctx) {
       JType newType = translate(x.getCastType());
@@ -82,6 +110,71 @@
     }
 
     @Override
+    public void endVisit(JMethodBody x, Context ctx) {
+      if (currentMethodBody.pop() != x) {
+        throw new RuntimeException("Unexpected JMethodBody popped");
+      }
+    }
+
+    /**
+     * Polymorphic dispatches to interfaces implemented by both a JSO and a
+     * regular type require special dispatch handling. If the instance is not a
+     * Java-derived object, the method from the single JSO implementation will
+     * be invoked. Otherwise, a polymorphic dispatch to the Java-derived object
+     * is made.
+     */
+    @Override
+    public void endVisit(JMethodCall x, Context ctx) {
+      JType targetClass = x.getTarget().getEnclosingType();
+      if (jsoSingleImpls.containsKey(targetClass)) {
+
+        SourceInfo info = x.getSourceInfo().makeChild(
+            JavaScriptObjectNormalizer.class,
+            "Polymorphic invocation of SingleJsoImpl interface");
+
+        // Find the method in the JSO type
+        JMethod jsoMethod = findJsoMethod(x.getTarget());
+        assert jsoMethod != null;
+
+        if (dualImpl.contains(targetClass)) {
+          /*
+           * This is the special-case code to handle interfaces.
+           */
+          JMultiExpression multi = new JMultiExpression(program, info);
+          JExpression instance = maybeMakeTempAssignment(multi, x.getInstance());
+
+          // instance.method(arg, arg)
+          JMethodCall localCall = new JMethodCall(program, info, instance,
+              x.getTarget());
+          localCall.getArgs().addAll(x.getArgs());
+
+          // instance.jsoMethod(arg, arg)
+          JMethodCall jsoCall = new JMethodCall(program, info, instance,
+              jsoMethod);
+          jsoCall.getArgs().addAll(x.getArgs());
+
+          // Cast.isJavaScriptObject() ? instance.jsoMethod() :
+          // instance.method();
+          JConditional newExpr = makeIsJsoConditional(info, instance,
+              x.getType(), jsoCall, localCall);
+
+          multi.exprs.add(newExpr);
+          // We may only have the ternary operation if there's no side-effect
+          ctx.replaceMe(multi.exprs.size() == 1 ? multi.exprs.get(0) : multi);
+        } else {
+          /*
+           * ... otherwise, if there's only a JSO implementation, we'll just
+           * call that directly.
+           */
+          JMethodCall jsoCall = new JMethodCall(program, info, x.getInstance(),
+              jsoMethod);
+          jsoCall.getArgs().addAll(x.getArgs());
+          ctx.replaceMe(jsoCall);
+        }
+      }
+    }
+
+    @Override
     public void endVisit(JNewArray x, Context ctx) {
       x.setType(translate(x.getType()));
     }
@@ -91,14 +184,74 @@
       x.setType(translate(x.getType()));
     }
 
+    @Override
+    public boolean visit(JMethodBody x, Context ctx) {
+      currentMethodBody.push(x);
+      return true;
+    }
+
+    private JMethod findJsoMethod(JMethod interfaceMethod) {
+      JClassType jsoClass = jsoSingleImpls.get(interfaceMethod.getEnclosingType());
+      assert program.isJavaScriptObject(jsoClass);
+      assert jsoClass != null;
+
+      JMethod toReturn = program.typeOracle.findConcreteImplementation(
+          interfaceMethod, jsoClass);
+      assert toReturn != null;
+      assert !toReturn.isAbstract();
+      assert jsoClass.isFinal() || toReturn.isFinal();
+
+      return toReturn;
+    }
+
+    private JConditional makeIsJsoConditional(SourceInfo info,
+        JExpression instance, JType conditionalType, JExpression isJsoExpr,
+        JExpression notJsoExpr) {
+      // Cast.isJavaScriptObjectOrString(instance)
+      JMethod isJavaScriptObjectMethod = program.getIndexedMethod("Cast.isJavaScriptObjectOrString");
+      JMethodCall isJavaScriptObjectExpr = new JMethodCall(program, info, null,
+          isJavaScriptObjectMethod);
+      isJavaScriptObjectExpr.getArgs().add(instance);
+      return new JConditional(program, info, conditionalType,
+          isJavaScriptObjectExpr, isJsoExpr, notJsoExpr);
+    }
+
+    private JExpression maybeMakeTempAssignment(JMultiExpression multi,
+        JExpression instance) {
+      if (instance.hasSideEffects()) {
+        /*
+         * It may be necessary to save off the instance expression into a local
+         * variable if its evaluation would produce side-effects. The
+         * multi-expression is used for this purpose.
+         */
+        SourceInfo info = instance.getSourceInfo().makeChild(
+            JavaScriptObjectNormalizer.class,
+            "Temporary assignment for instance with side-effects");
+        JLocal local = program.createLocal(info,
+            "maybeJsoInvocation".toCharArray(), instance.getType(), true,
+            currentMethodBody.peek());
+        multi.exprs.add(program.createAssignmentStmt(info,
+            new JLocalRef(program, info, local), instance).getExpr());
+
+        instance = new JLocalRef(program, info, local);
+      }
+      return instance;
+    }
+
     private JType translate(JType type) {
       if (program.isJavaScriptObject(type)) {
         return program.getJavaScriptObject();
+
+      } else if (jsoSingleImpls.containsKey(type) && !dualImpl.contains(type)) {
+        // Narrow to JSO when possible
+        return program.getJavaScriptObject();
+
       } else if (type instanceof JArrayType) {
         JArrayType arrayType = (JArrayType) type;
-        if (program.isJavaScriptObject(arrayType.getLeafType())) {
-          return program.getTypeArray(program.getJavaScriptObject(),
-              arrayType.getDims());
+        JType leafType = arrayType.getLeafType();
+        JType replacement = translate(leafType);
+        if (leafType != replacement) {
+          return program.getTypeArray(replacement, arrayType.getDims());
         }
       }
       return type;
@@ -109,15 +262,53 @@
     new JavaScriptObjectNormalizer(program).execImpl();
   }
 
+  /**
+   * Interfaces implemented both by a JSO type and a regular Java type.
+   */
+  private final Set<JInterfaceType> dualImpl;
+
+  /**
+   * Maps SingleJsoImpl interfaces onto the single JSO implementation.
+   */
+  private final Map<JInterfaceType, JClassType> jsoSingleImpls;
+
+  private final JClassType jsoType;
+
   private final JProgram program;
 
   private JavaScriptObjectNormalizer(JProgram program) {
     this.program = program;
+    dualImpl = program.typeOracle.getInterfacesWithJavaAndJsoImpls();
+    jsoSingleImpls = program.typeOracle.getSingleJsoImpls();
+
+    jsoType = program.getJavaScriptObject();
+    jsoType.implments.addAll(dualImpl);
   }
 
   private void execImpl() {
+    new Collector().accept(program);
+
     NormalizeVisitor visitor = new NormalizeVisitor();
     visitor.accept(program);
   }
 
+  private boolean isTagInterface(JReferenceType intr) {
+    if (!(intr instanceof JInterfaceType)) {
+      return false;
+    }
+
+    if (intr.methods.size() > 1) {
+      assert intr.methods.size() == 0
+          || intr.methods.get(0).getName().equals("$clinit");
+      return false;
+    }
+
+    for (JInterfaceType t : intr.implments) {
+      if (!isTagInterface(t)) {
+        return false;
+      }
+    }
+
+    return true;
+  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java b/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
index f40bfe9..95b748a 100644
--- a/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
+++ b/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
@@ -18,9 +18,12 @@
 import com.google.gwt.core.client.GWTBridge;
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.TreeLogger.Type;
 import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.core.ext.typeinfo.JMethod;
 import com.google.gwt.core.ext.typeinfo.JParameter;
+import com.google.gwt.core.ext.typeinfo.JType;
+import com.google.gwt.core.ext.typeinfo.NotFoundException;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.dev.javac.CompilationState;
 import com.google.gwt.dev.javac.CompilationUnit;
@@ -50,13 +53,15 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.SortedMap;
 import java.util.Stack;
+import java.util.TreeMap;
 import java.util.regex.Pattern;
 
 /**
  * An isolated {@link ClassLoader} for running all user code. All user files are
- * compiled from source code byte a {@link ByteCodeCompiler}. After
- * compilation, some byte code rewriting is performed to support
+ * compiled from source code byte a {@link ByteCodeCompiler}. After compilation,
+ * some byte code rewriting is performed to support
  * <code>JavaScriptObject</code> and its subtypes.
  * 
  * TODO: we should refactor this class to move the getClassInfoByDispId,
@@ -131,6 +136,23 @@
       DispatchClassInfo dispClassInfo = getClassInfoFromClassName(className);
       if (dispClassInfo != null) {
         String memberName = parsed.memberSignature();
+
+        /*
+         * Disallow the use of JSNI references to SingleJsoImpl interface
+         * methods. This policy is due to web-mode dispatch implementation
+         * details; resolving the JSNI reference wouldn't be just be a name
+         * replacement, instead it would be necessary to significantly alter the
+         * semantics of the hand-written JS.
+         */
+        if (singleJsoImplTypes.contains(canonicalizeClassName(parsed.className()))) {
+          logger.log(TreeLogger.WARN,
+              "Invalid JSNI reference to SingleJsoImpl interface ("
+                  + parsed.className() + "); consider using a trampoline. "
+                  + "Expect subsequent failures.", new NoSuchFieldError(
+                  jsniMemberRef));
+          return -1;
+        }
+
         int memberId = dispClassInfo.getMemberId(memberName);
         if (memberId < 0) {
           logger.log(TreeLogger.WARN, "Member '" + memberName
@@ -352,9 +374,10 @@
   private static final Class<?>[] BRIDGE_CLASSES = new Class<?>[] {
       ShellJavaScriptHost.class, GWTBridge.class};
 
-  private static final boolean CLASS_DUMP = false;
+  private static final boolean CLASS_DUMP = Boolean.getBoolean("gwt.dev.classDump");
 
-  private static final String CLASS_DUMP_PATH = "rewritten-classes";
+  private static final String CLASS_DUMP_PATH = System.getProperty(
+      "gwt.dev.classDumpPath", "rewritten-classes");
 
   private static boolean emmaAvailable = false;
 
@@ -503,6 +526,7 @@
 
   private ShellJavaScriptHost shellJavaScriptHost;
 
+  private final Set<String> singleJsoImplTypes = new HashSet<String>();
   /**
    * Used by {@link #findClass(String)} to prevent reentrant JSNI injection.
    */
@@ -543,17 +567,28 @@
       jsoTypes.add(jsoType);
 
       Set<String> jsoTypeNames = new HashSet<String>();
-      Map<String, String> jsoSuperTypes = new HashMap<String, String>();
+      Map<String, List<String>> jsoSuperTypes = new HashMap<String, List<String>>();
       for (JClassType type : jsoTypes) {
+        List<String> types = new ArrayList<String>();
+        types.add(getBinaryName(type.getSuperclass()));
+        for (JClassType impl : type.getImplementedInterfaces()) {
+          types.add(getBinaryName(impl));
+        }
+
         String binaryName = getBinaryName(type);
         jsoTypeNames.add(binaryName);
-        jsoSuperTypes.put(binaryName, getBinaryName(type.getSuperclass()));
+        jsoSuperTypes.put(binaryName, types);
       }
 
+      // computeSingleJsoImplData has two out parameters
+      SortedMap<String, com.google.gwt.dev.asm.commons.Method> mangledNamesToImplementations = new TreeMap<String, com.google.gwt.dev.asm.commons.Method>();
+      computeSingleJsoImplData(singleJsoImplTypes,
+          mangledNamesToImplementations);
+
       MyInstanceMethodOracle mapper = new MyInstanceMethodOracle(jsoTypes,
           typeOracle.getJavaLangObject());
       classRewriter = new HostedModeClassRewriter(jsoTypeNames, jsoSuperTypes,
-          mapper);
+          mangledNamesToImplementations, singleJsoImplTypes, mapper);
     } else {
       // If we couldn't find the JSO class, we don't need to do any rewrites.
       classRewriter = null;
@@ -598,8 +633,8 @@
    * was previously cached and has not been garbage collected.
    * 
    * @param javaObject the Object being wrapped
-   * @return the mapped wrapper, or <code>null</code> if the Java object
-   *         mapped or if the wrapper has been garbage collected
+   * @return the mapped wrapper, or <code>null</code> if the Java object mapped
+   *         or if the wrapper has been garbage collected
    */
   public Object getWrapperForObject(Object javaObject) {
     return weakJavaWrapperCache.get(javaObject);
@@ -727,6 +762,111 @@
     return lookupClassName;
   }
 
+  /**
+   * Cook up the data we need to support JSO subtypes that implement interfaces
+   * with methods. This includes the set of SingleJsoImpl interfaces actually
+   * implemented by a JSO type, the mangled method names, and the names of the
+   * Methods that should actually implement the virtual functions.
+   * 
+   * Given the current implementation of JSO$ and incremental execution of
+   * rebinds, it's not possible for Generators to produce additional
+   * JavaScriptObject subtypes, so this data can remain static.
+   */
+  private void computeSingleJsoImplData(
+      Set<String> singleJsoImplTypes,
+      SortedMap<String, com.google.gwt.dev.asm.commons.Method> mangledNamesToImplementations) {
+
+    // Loop over all types declared with the SingleJsoImpl annotation
+    typeLoop : for (JClassType type : typeOracle.getSingleJsoImplTypes()) {
+      assert type.isInterface() == type : "Expecting interfaces only";
+
+      /*
+       * By preemptively adding all possible mangled names by which a method
+       * could be called, we greatly simplify the logic necessary to rewrite the
+       * call-site.
+       * 
+       * interface A {void m();}
+       * 
+       * interface B extends A {void z();}
+       * 
+       * becomes
+       * 
+       * c_g_p_A_m() -> JsoA$.m$()
+       * 
+       * c_g_p_B_m() -> JsoA$.m$()
+       * 
+       * c_g_p_B_z() -> JsoB$.z$()
+       */
+      for (JMethod m : type.getOverridableMethods()) {
+        assert m.isAbstract() : "Expecting only abstract methods";
+
+        /*
+         * It is necessary to locate the implementing type on a per-method
+         * basis. Consider the case of
+         * 
+         * @SingleJsoImpl interface C extends A, B {}
+         * 
+         * Methods interited from interfaces A and B must be dispatched to their
+         * respective JSO implementations.
+         */
+        JClassType implementingType = findImplementingJsoType(m,
+            m.getEnclosingType().getSubtypes());
+
+        if (implementingType == null) {
+          /*
+           * This means that there is no concrete implementation of the
+           * interface by a JSO. Any implementation that might be created by a
+           * Generator won't be a JSO subtype, so we'll just ignore it as an
+           * actionable type. Were Generators ever able to create new JSO
+           * subtypes, we'd have to speculatively rewrite the callsite.
+           */
+          continue typeLoop;
+        }
+
+        /*
+         * Record the type as being actionable.
+         */
+        singleJsoImplTypes.add(canonicalizeClassName(getBinaryName(type)));
+
+        /*
+         * The mangled name adds the current interface like
+         * 
+         * com_foo_Bar_methodName
+         */
+        String mangledName = getBinaryName(type).replace('.', '_') + "_"
+            + m.getName();
+
+        /*
+         * Cook up the a pseudo-method declaration for the concrete type. This
+         * should look something like
+         * 
+         * ReturnType method$ (JsoType, ParamType, ParamType)
+         * 
+         * This must be kept in sync with the WriteJsoImpl class.
+         */
+        String decl = m.getReturnType().getParameterizedQualifiedSourceName()
+            + " " + m.getName() + "$ (" + getBinaryName(implementingType);
+        for (JParameter p : m.getParameters()) {
+          decl += ",";
+          decl += p.getType().getParameterizedQualifiedSourceName();
+        }
+        decl += ")";
+
+        com.google.gwt.dev.asm.commons.Method toImplement = com.google.gwt.dev.asm.commons.Method.getMethod(decl);
+
+        mangledNamesToImplementations.put(mangledName, toImplement);
+      }
+    }
+
+    if (logger.isLoggable(Type.SPAM)) {
+      TreeLogger dumpLogger = logger.branch(Type.SPAM,
+          "SingleJsoImpl method mappings");
+      for (Map.Entry<String, com.google.gwt.dev.asm.commons.Method> entry : mangledNamesToImplementations.entrySet()) {
+        dumpLogger.log(Type.SPAM, entry.getKey() + " -> " + entry.getValue());
+      }
+    }
+  }
+
   private byte[] findClassBytes(String className) {
     if (JavaScriptHost.class.getName().equals(className)) {
       // No need to rewrite.
@@ -735,7 +875,11 @@
 
     if (classRewriter != null && classRewriter.isJsoIntf(className)) {
       // Generate a synthetic JSO interface class.
-      return classRewriter.writeJsoIntf(className);
+      byte[] newBytes = classRewriter.writeJsoIntf(className);
+      if (CLASS_DUMP) {
+        classDump(className, newBytes);
+      }
+      return newBytes;
     }
 
     // A JSO impl class needs the class bytes for the original class.
@@ -812,6 +956,35 @@
     return classBytes;
   }
 
+  private JClassType findImplementingJsoType(JMethod toImplement,
+      JClassType[] types) {
+    JClassType jsoType = typeOracle.findType(JsValueGlue.JSO_CLASS);
+    JType[] paramTypes = new JType[toImplement.getParameters().length];
+    for (int i = 0, j = paramTypes.length; i < j; i++) {
+      paramTypes[i] = toImplement.getParameters()[i].getType();
+    }
+
+    for (JClassType type : types) {
+      if (type == null) {
+        continue;
+      } else if (type.isAbstract()) {
+        continue;
+      } else if (!type.isAssignableTo(jsoType)) {
+        continue;
+      } else {
+        try {
+          JMethod m = type.getMethod(toImplement.getName(), paramTypes);
+          if (!m.isAbstract()) {
+            return type;
+          }
+        } catch (NotFoundException e) {
+          // Ignore
+        }
+      }
+    }
+    return null;
+  }
+
   private String getBinaryName(JClassType type) {
     String name = type.getPackage().getName() + '.';
     name += type.getName().replace('.', '$');
diff --git a/dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java b/dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java
index 393c364..7b88173 100644
--- a/dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java
+++ b/dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java
@@ -19,13 +19,17 @@
 import com.google.gwt.dev.asm.ClassVisitor;
 import com.google.gwt.dev.asm.ClassWriter;
 import com.google.gwt.dev.asm.Opcodes;
+import com.google.gwt.dev.asm.commons.Method;
 import com.google.gwt.dev.shell.JsValueGlue;
 
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
+import java.util.ListIterator;
 import java.util.Map;
 import java.util.Set;
+import java.util.SortedMap;
 
 /**
  * This class performs any and all byte code rewriting needed to make hosted
@@ -87,7 +91,7 @@
   static final String JAVASCRIPTOBJECT_IMPL_DESC = JsValueGlue.JSO_IMPL_CLASS.replace(
       '.', '/');
 
-  static final String REFERENCE_FIELD = JsValueGlue.HOSTED_MODE_REFERENCE;;
+  static final String REFERENCE_FIELD = JsValueGlue.HOSTED_MODE_REFERENCE;
 
   static String addSyntheticThisParam(String owner, String methodDescriptor) {
     return "(L" + owner + ";" + methodDescriptor.substring(1);
@@ -112,13 +116,17 @@
   /**
    * Records the superclass of every JSO for generating empty JSO interfaces.
    */
-  private final Map<String, String> jsoSuperDescs;
+  private final Map<String, List<String>> jsoSuperDescs;
 
   /**
    * Maps methods to the class in which they are declared.
    */
   private InstanceMethodOracle mapper;
 
+  private final SortedMap<String, Method> mangledNamesToImplementations;
+
+  private final Set<String> singleJsoImplTypes;
+
   /**
    * Creates a new {@link HostedModeClassRewriter} for a specified set of
    * subclasses of JavaScriptObject.
@@ -128,21 +136,31 @@
    * @param mapper maps methods to the class in which they are declared
    */
   public HostedModeClassRewriter(Set<String> jsoSubtypes,
-      Map<String, String> jsoSuperTypes, InstanceMethodOracle mapper) {
+      Map<String, List<String>> jsoSuperTypes,
+      SortedMap<String, Method> mangledNamesToImplementations,
+      Set<String> singleJsoImplTypes, InstanceMethodOracle mapper) {
     Set<String> buildJsoIntfDescs = new HashSet<String>();
     Set<String> buildJsoImplDescs = new HashSet<String>();
-    Map<String, String> buildJsoSuperDescs = new HashMap<String, String>();
+    Map<String, List<String>> buildJsoSuperDescs = new HashMap<String, List<String>>();
     for (String jsoSubtype : jsoSubtypes) {
       String desc = toDescriptor(jsoSubtype);
       buildJsoIntfDescs.add(desc);
       buildJsoImplDescs.add(desc + "$");
-      String superType = jsoSuperTypes.get(jsoSubtype);
-      assert (superType != null);
-      buildJsoSuperDescs.put(desc, toDescriptor(superType));
+
+      List<String> superTypes = jsoSuperTypes.get(jsoSubtype);
+      assert (superTypes != null);
+      assert (superTypes.size() > 0);
+      for (ListIterator<String> i = superTypes.listIterator(); i.hasNext();) {
+        i.set(toDescriptor(i.next()));
+      }
+      buildJsoSuperDescs.put(desc, Collections.unmodifiableList(superTypes));
     }
+
     this.jsoIntfDescs = Collections.unmodifiableSet(buildJsoIntfDescs);
     this.jsoImplDescs = Collections.unmodifiableSet(buildJsoImplDescs);
     this.jsoSuperDescs = Collections.unmodifiableMap(buildJsoSuperDescs);
+    this.mangledNamesToImplementations = Collections.unmodifiableSortedMap(mangledNamesToImplementations);
+    this.singleJsoImplTypes = Collections.unmodifiableSet(singleJsoImplTypes);
     this.mapper = mapper;
   }
 
@@ -182,10 +200,14 @@
     // v = new CheckClassAdapter(v);
     // v = new TraceClassVisitor(v, new PrintWriter(System.out));
 
+    v = new RewriteSingleJsoImplDispatches(v, singleJsoImplTypes,
+        mangledNamesToImplementations);
+
     v = new RewriteRefsToJsoClasses(v, jsoIntfDescs, mapper);
 
     if (jsoImplDescs.contains(desc)) {
-      v = new WriteJsoImpl(v, jsoIntfDescs, mapper);
+      v = WriteJsoImpl.create(v, desc, jsoIntfDescs, mapper,
+          mangledNamesToImplementations);
     }
 
     v = new RewriteJsniMethods(v, anonymousClassMap);
@@ -202,8 +224,9 @@
     String desc = toDescriptor(className);
     assert (jsoIntfDescs.contains(desc));
     assert (jsoSuperDescs.containsKey(desc));
-    String superDesc = jsoSuperDescs.get(desc);
-    assert (superDesc != null);
+    List<String> superDescs = jsoSuperDescs.get(desc);
+    assert (superDescs != null);
+    assert (superDescs.size() > 0);
 
     // The ASM model is to chain a bunch of visitors together.
     ClassWriter writer = new ClassWriter(0);
@@ -213,10 +236,11 @@
     // v = new TraceClassVisitor(v, new PrintWriter(System.out));
 
     String[] interfaces;
-    if ("java/lang/Object".equals(superDesc)) {
+    // TODO(bov): something better than linear?
+    if (superDescs.contains("java/lang/Object")) {
       interfaces = null;
     } else {
-      interfaces = new String[] {superDesc};
+      interfaces = superDescs.toArray(new String[superDescs.size()]);
     }
     v.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC | Opcodes.ACC_INTERFACE, desc,
         null, "java/lang/Object", interfaces);
diff --git a/dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteSingleJsoImplDispatches.java b/dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteSingleJsoImplDispatches.java
new file mode 100644
index 0000000..adb6326
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteSingleJsoImplDispatches.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.shell.rewrite;
+
+import com.google.gwt.dev.asm.ClassAdapter;
+import com.google.gwt.dev.asm.ClassVisitor;
+import com.google.gwt.dev.asm.MethodAdapter;
+import com.google.gwt.dev.asm.MethodVisitor;
+import com.google.gwt.dev.asm.Opcodes;
+import com.google.gwt.dev.asm.Type;
+import com.google.gwt.dev.asm.commons.Method;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Effects the renaming of {@code @SingleJsoImpl} methods from their original
+ * name to their mangled name. Let us call the original method an "unmangled
+ * method" and the new method a "mangled method". There are three steps in this
+ * process:
+ * <ol>
+ * <li>Within {@code @SingleJsoImpl} interfaces rename all unmangled methods to
+ * become mangled methods.</li>
+ * <li>Within non-JSO classes containing a concrete implementation of an
+ * unmangled method, add a mangled method which is implemented as a simple
+ * trampoline to the unmangled method. (We don't do this in JSO classes here
+ * because the one-and-only trampoline lives in JavaScriptObject$ and is emitted
+ * in {@link WriteJsoImpl}).
+ * <li>Update all call sites targeting unmangled methods to target mangled
+ * methods instead, provided the caller is binding to the interface rather than
+ * a concrete type.</li>
+ * </ol>
+ */
+public class RewriteSingleJsoImplDispatches extends ClassAdapter {
+  private class MyMethodVisitor extends MethodAdapter {
+    public MyMethodVisitor(MethodVisitor mv) {
+      super(mv);
+    }
+
+    /*
+     * Implements objective #3: updates call sites to unmangled methods.
+     */
+    @Override
+    public void visitMethodInsn(int opcode, String owner, String name,
+        String desc) {
+      if (singleJsoImplTypes.contains(owner)) {
+        name = owner.replace('/', '_') + "_" + name;
+      }
+
+      super.visitMethodInsn(opcode, owner, name, desc);
+    }
+  }
+
+  private String currentTypeName;
+  private final Set<String> implementedMethods = new HashSet<String>();
+  private final SortedMap<String, Method> mangledNamesToImplementations;
+  private final Set<String> singleJsoImplTypes;
+  private boolean inSingleJsoImplInterfaceType;
+
+  public RewriteSingleJsoImplDispatches(ClassVisitor v,
+      Set<String> singleJsoImplTypes,
+      SortedMap<String, Method> mangledNamesToImplementations) {
+    super(v);
+    this.singleJsoImplTypes = Collections.unmodifiableSet(singleJsoImplTypes);
+    this.mangledNamesToImplementations = Collections.unmodifiableSortedMap(mangledNamesToImplementations);
+  }
+
+  @Override
+  public void visit(int version, int access, String name, String signature,
+      String superName, String[] interfaces) {
+    assert currentTypeName == null;
+    super.visit(version, access, name, signature, superName, interfaces);
+
+    currentTypeName = name;
+    inSingleJsoImplInterfaceType = singleJsoImplTypes.contains(name);
+
+    /*
+     * Implements objective #2: non-JSO types that implement a SingleJsoImpl
+     * interface don't have their original instance methods altered. Instead, we
+     * add trampoline methods with mangled names that simply call over to the
+     * original methods.
+     */
+    if (interfaces != null && (access & Opcodes.ACC_INTERFACE) == 0) {
+      List<String> toStub = new ArrayList<String>();
+      Collections.addAll(toStub, interfaces);
+      toStub.retainAll(singleJsoImplTypes);
+
+      for (String stubIntr : toStub) {
+        writeTrampoline(stubIntr);
+      }
+    }
+  }
+
+  @Override
+  public void visitEnd() {
+    /*
+     * Add any missing methods that are defined by a super-interface, but that
+     * may be referenced via a more specific interface.
+     */
+    if (inSingleJsoImplInterfaceType) {
+      for (Map.Entry<String, Method> entry : toImplement(currentTypeName).entrySet()) {
+        writeEmptyMethod(entry.getKey(), entry.getValue());
+      }
+    }
+    super.visitEnd();
+  }
+
+  @Override
+  public MethodVisitor visitMethod(int access, String name, String desc,
+      String signature, String[] exceptions) {
+
+    /*
+     * Implements objective #2: Rename unmangled methods in a @SingleJsoImpl
+     * into mangled methods (except for clinit, LOL).
+     */
+    if (inSingleJsoImplInterfaceType && !"<clinit>".equals(name)) {
+      name = currentTypeName.replace('/', '_') + "_" + name;
+      implementedMethods.add(name);
+    }
+
+    MethodVisitor mv = super.visitMethod(access, name, desc, signature,
+        exceptions);
+    if (mv == null) {
+      return null;
+    }
+
+    return new MyMethodVisitor(mv);
+  }
+
+  /**
+   * Given a resource name of a class, find all mangled method names that must
+   * be implemented.
+   */
+  private SortedMap<String, Method> toImplement(String typeName) {
+    String name = typeName.replace('/', '_');
+    String prefix = name + "_";
+    String suffix = name + "`";
+    SortedMap<String, Method> toReturn = new TreeMap<String, Method>(
+        mangledNamesToImplementations.subMap(prefix, suffix));
+    toReturn.keySet().removeAll(implementedMethods);
+    return toReturn;
+  }
+
+  private void writeEmptyMethod(String mangledMethodName, Method method) {
+    assert method.getArgumentTypes().length > 0;
+    // Remove the first argument, which would be the implementing JSO type
+    String descriptor = "("
+        + method.getDescriptor().substring(
+            1 + method.getArgumentTypes()[0].getDescriptor().length());
+
+    // Create the stub method entry in the interface
+    MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC
+        | Opcodes.ACC_ABSTRACT, mangledMethodName, descriptor, null, null);
+    mv.visitEnd();
+  }
+
+  /**
+   * For regular Java objects that implement a SingleJsoImpl interface, write
+   * instance trampoline dispatchers for mangled method names to the
+   * implementing method.
+   */
+  private void writeTrampoline(String stubIntr) {
+    /*
+     * This is almost the same kind of trampoline as the ones generated in
+     * WriteJsoImpl, however there are enough small differences between the
+     * semantics of the dispatches that would make a common implementation far
+     * more awkward than the duplication of code.
+     */
+    for (Map.Entry<String, Method> entry : toImplement(stubIntr).entrySet()) {
+      String mangledName = entry.getKey();
+      Method method = entry.getValue();
+
+      String descriptor = "("
+          + method.getDescriptor().substring(
+              1 + method.getArgumentTypes()[0].getDescriptor().length());
+      String localName = method.getName().substring(0,
+          method.getName().length() - 1);
+      Method toCall = new Method(localName, descriptor);
+
+      // Must not be final
+      MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC
+          | Opcodes.ACC_SYNTHETIC, mangledName, descriptor, null, null);
+      if (mv != null) {
+        mv.visitCode();
+
+        /*
+         * It just so happens that the stack and local variable sizes are the
+         * same, but they're kept distinct to aid in clarity should the dispatch
+         * logic change.
+         * 
+         * These start at 1 because we need to load "this" onto the stack
+         */
+        int var = 1;
+        int size = 1;
+
+        // load this
+        mv.visitVarInsn(Opcodes.ALOAD, 0);
+
+        // then the rest of the arguments
+        for (Type t : toCall.getArgumentTypes()) {
+          size += t.getSize();
+          mv.visitVarInsn(t.getOpcode(Opcodes.ILOAD), var);
+          var += t.getSize();
+        }
+        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, currentTypeName,
+            toCall.getName(), toCall.getDescriptor());
+        mv.visitInsn(toCall.getReturnType().getOpcode(Opcodes.IRETURN));
+        mv.visitMaxs(size, var);
+        mv.visitEnd();
+      }
+    }
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/rewrite/WriteJsoImpl.java b/dev/core/src/com/google/gwt/dev/shell/rewrite/WriteJsoImpl.java
index 54bced6..4f1e265 100644
--- a/dev/core/src/com/google/gwt/dev/shell/rewrite/WriteJsoImpl.java
+++ b/dev/core/src/com/google/gwt/dev/shell/rewrite/WriteJsoImpl.java
@@ -20,36 +20,201 @@
 import com.google.gwt.dev.asm.FieldVisitor;
 import com.google.gwt.dev.asm.MethodVisitor;
 import com.google.gwt.dev.asm.Opcodes;
+import com.google.gwt.dev.asm.Type;
+import com.google.gwt.dev.asm.commons.Method;
 import com.google.gwt.dev.shell.rewrite.HostedModeClassRewriter.InstanceMethodOracle;
 
 import java.util.ArrayList;
+import java.util.Map;
 import java.util.Set;
 
 /**
- * Writes the implementation class for a JSO type.
+ * Writes the implementation classes for JSO and its subtypes.
  * 
+ * Changes made by the base class:
  * <ol>
  * <li>The new type has the same name as the old type with a '$' appended.</li>
- * <li>The new type's superclass is Object.</li>
- * <li>All instance methods in the original type become static methods taking
- * an explicit <code>this</code> parameter. Such methods have the same stack
+ * <li>All instance methods in the original type become static methods taking an
+ * explicit <code>this</code> parameter. Such methods have the same stack
  * behavior as the original.</li>
- * <li>JavaScriptObject itself gets a new synthetic field to store the
- * underlying hosted mode reference.</li>
  * </ol>
  */
-class WriteJsoImpl extends ClassAdapter {
+abstract class WriteJsoImpl extends ClassAdapter {
 
   /**
-   * An unmodifiable set of descriptors containing <code>JavaScriptObject</code>
-   * and all subclasses.
+   * This type implements JavaScriptObject.
+   * 
+   * <ol>
+   * <li>JavaScriptObject itself gets a new synthetic field to store the
+   * underlying hosted mode reference.</li>
+   * <li>Instance methods are added so that JavaScriptObject implements all
+   * SingleJsoImpl interfaces.</li>
+   * </ol>
+   * 
    */
-  protected final Set<String> jsoDescriptors;
+  private static class ForJsoDollar extends WriteJsoImpl {
+    /**
+     * An unmodifiable set of descriptors containing
+     * <code>JavaScriptObject</code> and all subclasses.
+     */
+    private final Set<String> jsoDescriptors;
+    private final Map<String, Method> methodsToImplement;
+
+    public ForJsoDollar(ClassVisitor cv, Set<String> jsoDescriptors,
+        InstanceMethodOracle mapper, Map<String, Method> methodsToImplement) {
+      super(cv, mapper);
+      this.jsoDescriptors = jsoDescriptors;
+      this.methodsToImplement = methodsToImplement;
+    }
+
+    @Override
+    public void visit(int version, int access, String name, String signature,
+        String superName, String[] interfaces) {
+
+      ArrayList<String> jsoDescList = new ArrayList<String>();
+      jsoDescList.addAll(jsoDescriptors);
+      interfaces = jsoDescList.toArray(new String[jsoDescList.size()]);
+
+      super.visit(version, access, name, signature, superName, interfaces);
+
+      /*
+       * Generate the synthetic "hostedModeReferece" field to contain the
+       * underlying real reference to the JavaScript object.
+       */
+      FieldVisitor fv = visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC,
+          HostedModeClassRewriter.REFERENCE_FIELD, "Ljava/lang/Object;", null,
+          null);
+      if (fv != null) {
+        fv.visitEnd();
+      }
+
+      // Implement the trampoline methods
+      for (Map.Entry<String, Method> entry : methodsToImplement.entrySet()) {
+        writeTrampoline(entry.getKey(), entry.getValue());
+      }
+    }
+
+    /**
+     * JSO methods are implemented as flyweight style, with the instance being
+     * passed as the first parameter. This loop create instance methods on JSO$
+     * for all of the mangled SingleJsoImpl interface method names. These
+     * instance methods simply turn around and call the static-dispatch methods.
+     * In Java, it might look like:
+     * 
+     * <pre>
+     * public String com_google_Interface_someMethod(int a, double b) {
+     *   return com.google.MyJso$.someMethod$(this, a, b);
+     * }
+     * </pre>
+     * 
+     * @param mangledName {@code com_google_gwt_sample_hello_client_Interface_a}
+     * @param implementingMethod {@code static final java.lang.String
+     *          a$(com.google.gwt.sample.hello.client.Jso, ...);}
+     */
+    private void writeTrampoline(String mangledName, Method implementingMethod) {
+      /*
+       * We derive the local descriptor by simply removing the first argument
+       * from the static method we want to call.
+       */
+      assert implementingMethod.getArgumentTypes().length > 0;
+      String localDescriptor = "("
+          + implementingMethod.getDescriptor().substring(
+              1 + implementingMethod.getArgumentTypes()[0].getDescriptor().length());
+      Method localMethod = new Method(mangledName, localDescriptor);
+
+      /*
+       * We also use the first argument to know which type to statically
+       * dispatch to.
+       */
+      Type implementingType = Type.getType("L"
+          + implementingMethod.getArgumentTypes()[0].getInternalName() + "$;");
+
+      // Maybe create the method. This is marked final as a sanity check
+      MethodVisitor mv = visitMethodNoRewrite(Opcodes.ACC_PUBLIC
+          | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC, localMethod.getName(),
+          localMethod.getDescriptor(), null, null);
+
+      if (mv != null) {
+        mv.visitCode();
+
+        /*
+         * It just so happens that the stack and local variable sizes are the
+         * same, but they're kept distinct to aid in clarity should the dispatch
+         * logic change.
+         */
+        int var = 0;
+        int size = 0;
+
+        for (Type t : implementingMethod.getArgumentTypes()) {
+          size += t.getSize();
+          mv.visitVarInsn(t.getOpcode(Opcodes.ILOAD), var);
+          var += t.getSize();
+        }
+        mv.visitMethodInsn(Opcodes.INVOKESTATIC,
+            implementingType.getInternalName(), implementingMethod.getName(),
+            implementingMethod.getDescriptor());
+        mv.visitInsn(localMethod.getReturnType().getOpcode(Opcodes.IRETURN));
+        mv.visitMaxs(size, var);
+        mv.visitEnd();
+      }
+    }
+  }
+
+  /**
+   * This type is used to implement subtypes of JSO.
+   * 
+   * <ol>
+   * <li>The new type's superclass is mangled by adding $.</li>
+   * <li>Constructors are deleted.</li>
+   * </ol>
+   */
+  private static class ForJsoInterface extends WriteJsoImpl {
+    public ForJsoInterface(ClassVisitor cv, InstanceMethodOracle mapper) {
+      super(cv, mapper);
+    }
+
+    @Override
+    public void visit(int version, int access, String name, String signature,
+        String superName, String[] interfaces) {
+      // Reference the old superclass's implementation class.
+      superName += '$';
+      interfaces = null;
+
+      super.visit(version, access, name, signature, superName, interfaces);
+    }
+
+    @Override
+    public MethodVisitor visitMethod(int access, String name, String desc,
+        String signature, String[] exceptions) {
+      boolean isCtor = isCtor(name);
+      if (isCtor) {
+        // Don't copy over constructors except for JavaScriptObject itself.
+        return null;
+      }
+      return super.visitMethod(access, name, desc, signature, exceptions);
+    }
+  }
+
+  /**
+   * Creates a ClassVisitor to implement a JavaScriptObject subtype. This will
+   * select between a simple implementation for user-defined JSO subtypes and
+   * the complex implementation for implementing JavaScriptObject$.
+   */
+  public static ClassVisitor create(ClassVisitor cv, String classDescriptor,
+      Set<String> jsoDescriptors, InstanceMethodOracle mapper,
+      Map<String, Method> methodsToImplement) {
+
+    if (classDescriptor.equals(HostedModeClassRewriter.JAVASCRIPTOBJECT_IMPL_DESC)) {
+      return new ForJsoDollar(cv, jsoDescriptors, mapper, methodsToImplement);
+    } else {
+      return new ForJsoInterface(cv, mapper);
+    }
+  }
 
   /**
    * Maps methods to the class in which they are declared.
    */
-  private InstanceMethodOracle mapper;
+  private final InstanceMethodOracle mapper;
 
   /**
    * The original name of the class being visited.
@@ -64,67 +229,60 @@
    *          <code>JavaScriptObject</code> and all subclasses
    * @param mapper maps methods to the class in which they are declared
    */
-  public WriteJsoImpl(ClassVisitor cv, Set<String> jsoDescriptors,
-      InstanceMethodOracle mapper) {
+  private WriteJsoImpl(ClassVisitor cv, InstanceMethodOracle mapper) {
     super(cv);
-    this.jsoDescriptors = jsoDescriptors;
     this.mapper = mapper;
   }
 
+  /**
+   * Records the original name and resets access opcodes.
+   */
   @Override
   public void visit(int version, int access, String name, String signature,
       String superName, String[] interfaces) {
     originalName = name;
-
-    // JavaScriptObject$ must implement all JSO interface types.
-    if (isJavaScriptObject()) {
-      ArrayList<String> jsoDescList = new ArrayList<String>();
-      jsoDescList.addAll(jsoDescriptors);
-      interfaces = jsoDescList.toArray(new String[jsoDescList.size()]);
-    } else {
-      // Reference the old superclass's implementation class.
-      superName += '$';
-      interfaces = null;
-    }
-
-    super.visit(version, access, name + '$', signature, superName, interfaces);
-
-    if (isJavaScriptObject()) {
-      FieldVisitor fv = visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC,
-          HostedModeClassRewriter.REFERENCE_FIELD, "Ljava/lang/Object;", null,
-          null);
-      if (fv != null) {
-        fv.visitEnd();
-      }
-    }
+    super.visit(version, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC
+        | Opcodes.ACC_SYNTHETIC, name + '$', signature, superName, interfaces);
   }
 
+  /**
+   * Mangle all instance methods declared in JavaScriptObject types.
+   */
   @Override
   public MethodVisitor visitMethod(int access, String name, String desc,
       String signature, String[] exceptions) {
-    boolean isCtor = "<init>".equals(name);
-    if (!isJavaScriptObject() && isCtor) {
-      // Don't copy over constructors except for JavaScriptObject itself.
-      return null;
-    }
+    boolean isCtor = isCtor(name);
     if (!isCtor && !isStatic(access) && !isObjectMethod(name + desc)) {
       access |= Opcodes.ACC_STATIC;
-      desc = HostedModeClassRewriter.addSyntheticThisParam(originalName, desc);
+      desc = HostedModeClassRewriter.addSyntheticThisParam(getOriginalName(),
+          desc);
       name = name + "$";
     }
     return super.visitMethod(access, name, desc, signature, exceptions);
   }
 
-  private boolean isJavaScriptObject() {
-    return originalName.equals(HostedModeClassRewriter.JAVASCRIPTOBJECT_DESC);
+  protected String getOriginalName() {
+    return originalName;
   }
 
-  private boolean isObjectMethod(String signature) {
-    return "java/lang/Object".equals(mapper.findOriginalDeclaringClass(originalName,
-        signature));
+  protected boolean isCtor(String name) {
+    return "<init>".equals(name);
   }
 
-  private boolean isStatic(int access) {
+  protected boolean isObjectMethod(String signature) {
+    return "java/lang/Object".equals(mapper.findOriginalDeclaringClass(
+        originalName, signature));
+  }
+
+  protected boolean isStatic(int access) {
     return (access & Opcodes.ACC_STATIC) != 0;
   }
+
+  /**
+   * Allows access to an unmodified visitMethod call.
+   */
+  protected MethodVisitor visitMethodNoRewrite(int access, String name,
+      String desc, String signature, String[] exceptions) {
+    return super.visitMethod(access, name, desc, signature, exceptions);
+  }
 }
diff --git a/dev/core/super/com/google/gwt/core/client/SingleJsoImpl.java b/dev/core/super/com/google/gwt/core/client/SingleJsoImpl.java
new file mode 100644
index 0000000..bbc2a55
--- /dev/null
+++ b/dev/core/super/com/google/gwt/core/client/SingleJsoImpl.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.core.client;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * The presence of this annotation on an interface type allows one
+ * JavaScriptObject subclass to implement the interface. Any number of types
+ * that are not derived from JavaScriptObject may also implement the interface.
+ * <p>
+ * The use of the SingleJsoImpl annotation is subject to the following
+ * restrictions:
+ * <ul>
+ * <li>Must be applied only to interface types</li>
+ * <li>Any super-interfaces of the annotated interface must also be annotated
+ * with the SingleJsoImpl</li>
+ * </ul>
+ */
+@Documented
+@Target(ElementType.TYPE)
+public @interface SingleJsoImpl {
+}
\ No newline at end of file
diff --git a/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/Cast.java b/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/Cast.java
index 6e4e378..6310a50 100644
--- a/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/Cast.java
+++ b/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/Cast.java
@@ -52,6 +52,17 @@
   }
 
   /**
+   * Allow a dynamic cast to an object, always succeeding if it's a JSO.
+   */
+  static Object dynamicCastAllowJso(Object src, int dstId) {
+    if (src != null && !isJavaScriptObject(src) &&
+        !canCastUnsafe(Util.getTypeId(src), dstId)) {
+      throw new ClassCastException();
+    }
+    return src;
+  }
+  
+  /**
    * Allow a cast to JSO only if there's no type ID.
    */
   static Object dynamicCastJso(Object src) {
@@ -72,6 +83,15 @@
     return (src != null) && isJavaScriptObject(src);
   }
 
+  /**
+   * Returns true if the object is a Java object and can be cast, or if it's a
+   * non-null JSO.
+   */
+  static boolean instanceOfOrJso(Object src, int dstId) {
+    return (src != null) &&
+        (isJavaScriptObject(src) || canCast(Util.getTypeId(src), dstId));
+  }
+
   static boolean isJavaObject(Object src) {
     return Util.getTypeMarker(src) == getNullMethod() || Util.getTypeId(src) == 2;
   }
@@ -80,6 +100,10 @@
     return Util.getTypeMarker(src) != getNullMethod() && Util.getTypeId(src) != 2;
   }
 
+  static boolean isJavaScriptObjectOrString(Object src) {
+    return Util.getTypeMarker(src) != getNullMethod();
+  }
+
   /**
    * Uses the not operator to perform a null-check; do NOT use on anything that
    * could be a String.
diff --git a/dev/core/test/com/google/gwt/dev/javac/JSORestrictionsTest.java b/dev/core/test/com/google/gwt/dev/javac/JSORestrictionsTest.java
index b8eb521..836dca5 100644
--- a/dev/core/test/com/google/gwt/dev/javac/JSORestrictionsTest.java
+++ b/dev/core/test/com/google/gwt/dev/javac/JSORestrictionsTest.java
@@ -20,8 +20,34 @@
 
 import junit.framework.TestCase;
 
+/**
+ * Tests the JSORestrictionsChecker.
+ */
 public class JSORestrictionsTest extends TestCase {
 
+  static {
+    // Mac -XstartOnFirstThread bug
+    if (Thread.currentThread().getContextClassLoader() == null) {
+      Thread.currentThread().setContextClassLoader(
+          JSORestrictionsTest.class.getClassLoader());
+    }
+  }
+
+  public void testAnnotationOnClass() {
+    StringBuffer buggyCode = new StringBuffer();
+    buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+    buggyCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+    buggyCode.append("public class Buggy {\n");
+    buggyCode.append("  @SingleJsoImpl \n");
+    buggyCode.append("  static class Squeaks {\n");
+    buggyCode.append("    public void squeak() {}\n");
+    buggyCode.append("  }\n");
+    buggyCode.append("}\n");
+
+    shouldGenerateError(buggyCode, "Line 5: "
+        + JSORestrictionsChecker.ERR_ONLY_INTERFACES);
+  }
+
   public void testFinalClass() {
     StringBuffer code = new StringBuffer();
     code.append("import com.google.gwt.core.client.JavaScriptObject;\n");
@@ -33,6 +59,33 @@
     shouldGenerateNoError(code);
   }
 
+  public void testImplementsInterfaces() {
+    StringBuffer goodCode = new StringBuffer();
+    goodCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+    goodCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+    goodCode.append("public class Buggy {\n");
+    goodCode.append("  @SingleJsoImpl\n");
+    goodCode.append("  static interface Squeaks {\n");
+    goodCode.append("    public void squeak();\n");
+    goodCode.append("  }\n");
+    goodCode.append("  @SingleJsoImpl \n");
+    goodCode.append("  static interface Squeaks2 extends Squeaks {\n");
+    goodCode.append("    public void squeak();\n");
+    goodCode.append("    public void squeak2();\n");
+    goodCode.append("  }\n");
+    goodCode.append("  static class Squeaker extends JavaScriptObject implements Squeaks {\n");
+    goodCode.append("    public final void squeak() { }\n");
+    goodCode.append("    protected Squeaker() { }\n");
+    goodCode.append("  }\n");
+    goodCode.append("  static class Squeaker2 extends Squeaker implements Squeaks, Squeaks2 {\n");
+    goodCode.append("    public final void squeak2() { }\n");
+    goodCode.append("    protected Squeaker2() { }\n");
+    goodCode.append("  }\n");
+    goodCode.append("}\n");
+
+    shouldGenerateNoError(goodCode);
+  }
+
   public void testInstanceField() {
     StringBuffer buggyCode = new StringBuffer();
     buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
@@ -56,6 +109,32 @@
         + JSORestrictionsChecker.ERR_CONSTRUCTOR_WITH_PARAMETERS);
   }
 
+  public void testMultipleImplementations() {
+    StringBuffer buggyCode = new StringBuffer();
+    buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+    buggyCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+    buggyCode.append("public class Buggy {\n");
+    buggyCode.append("  @SingleJsoImpl\n");
+    buggyCode.append("  static interface Squeaks {\n");
+    buggyCode.append("    public void squeak();\n");
+    buggyCode.append("  }\n");
+    buggyCode.append("  static class Squeaker extends JavaScriptObject implements Squeaks {\n");
+    buggyCode.append("    public final void squeak() { }\n");
+    buggyCode.append("    protected Squeaker() { }\n");
+    buggyCode.append("  }\n");
+    buggyCode.append("  static class Squeaker2 extends JavaScriptObject implements Squeaks {\n");
+    buggyCode.append("    public final void squeak() { }\n");
+    buggyCode.append("    protected Squeaker2() { }\n");
+    buggyCode.append("  }\n");
+    buggyCode.append("}\n");
+
+    shouldGenerateError(buggyCode, "Line 8: "
+        + JSORestrictionsChecker.errAlreadyImplemented("Buggy$Squeaks",
+            "Buggy$Squeaker", "Buggy$Squeaker2"), "Line 12: "
+        + JSORestrictionsChecker.errAlreadyImplemented("Buggy$Squeaks",
+            "Buggy$Squeaker", "Buggy$Squeaker2"));
+  }
+
   public void testNew() {
     StringBuffer buggyCode = new StringBuffer();
     buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
@@ -70,18 +149,65 @@
         + JSORestrictionsChecker.ERR_NEW_JSO);
   }
 
-  public void testNoConstructor() {
-    StringBuffer buggyCode = new StringBuffer();
-    buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
-    buggyCode.append("public class Buggy extends JavaScriptObject {\n");
-    buggyCode.append("}\n");
+  public void testNoAnnotationOnInterfaceSubtype() {
+    StringBuffer goodCode = new StringBuffer();
+    goodCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+    goodCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+    goodCode.append("public class Buggy {\n");
+    goodCode.append("  @SingleJsoImpl\n");
+    goodCode.append("  static interface Squeaks {\n");
+    goodCode.append("    public void squeak();\n");
+    goodCode.append("  }\n");
+    goodCode.append("  /* @SingleJsoImpl */\n");
+    goodCode.append("  static interface Sub extends Squeaks {\n");
+    goodCode.append("  }\n");
+    goodCode.append("}\n");
 
-    // The public constructor is implicit.
-    shouldGenerateError(buggyCode, "Line 2: "
-        + JSORestrictionsChecker.ERR_NONPROTECTED_CONSTRUCTOR);
+    shouldGenerateNoError(goodCode);
   }
 
-  public void testNoInterfaces() {
+  public void testNoAnnotationOnInterfaceSupertype() {
+    StringBuffer buggyCode = new StringBuffer();
+    buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+    buggyCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+    buggyCode.append("public class Buggy {\n");
+    buggyCode.append("  /* @SingleJsoImpl */ \n");
+    buggyCode.append("  static interface Squeaks {\n");
+    buggyCode.append("    public void squeak();\n");
+    buggyCode.append("  }\n");
+    buggyCode.append("  @SingleJsoImpl \n");
+    buggyCode.append("  static interface Sub extends Squeaks {\n");
+    buggyCode.append("  }\n");
+    buggyCode.append("}\n");
+
+    shouldGenerateError(buggyCode, "Line 9: "
+        + JSORestrictionsChecker.ERR_INTF_EXTENDS_BAD_INTF);
+  }
+
+  public void testNoAnnotationOnInterfaceSupertypeSandwich() {
+    StringBuffer buggyCode = new StringBuffer();
+    buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+    buggyCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+    buggyCode.append("public class Buggy {\n");
+    buggyCode.append("  @SingleJsoImpl\n");
+    buggyCode.append("  static interface Squeaks {\n");
+    buggyCode.append("    public void squeak();\n");
+    buggyCode.append("  }\n");
+    buggyCode.append("  /* @SingleJsoImpl */\n");
+    buggyCode.append("  static interface Squeaks2 extends Squeaks {\n");
+    buggyCode.append("    public void squeak2();\n");
+    buggyCode.append("  }\n");
+    buggyCode.append("  @SingleJsoImpl\n");
+    buggyCode.append("  static interface Squeaks3 extends Squeaks2 {\n");
+    buggyCode.append("    public void squeak3();\n");
+    buggyCode.append("  }\n");
+    buggyCode.append("}\n");
+
+    shouldGenerateError(buggyCode, "Line 13: "
+        + JSORestrictionsChecker.ERR_INTF_EXTENDS_BAD_INTF);
+  }
+
+  public void testNoAnnotationOnOtherwiseValidInterfaces() {
     StringBuffer buggyCode = new StringBuffer();
     buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
     buggyCode.append("public class Buggy {\n");
@@ -94,8 +220,46 @@
     buggyCode.append("  }\n");
     buggyCode.append("}\n");
 
-    shouldGenerateError(buggyCode, "Line 6: "
-        + JSORestrictionsChecker.errInterfaceWithMethods("Buggy.Squeaks"));
+    shouldGenerateError(
+        buggyCode,
+        "Line 6: "
+            + JSORestrictionsChecker.errInterfaceWithMethodsNoAnnotation(("Buggy$Squeaks")));
+  }
+
+  public void testNoAnnotationOnOtherwiseValidDerivedInterfaces() {
+    StringBuffer buggyCode = new StringBuffer();
+    buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+    buggyCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+    buggyCode.append("public class Buggy {\n");
+    buggyCode.append("  @SingleJsoImpl\n");
+    buggyCode.append("  static interface Squeaks {\n");
+    buggyCode.append("    public void squeak();\n");
+    buggyCode.append("  }\n");
+    buggyCode.append("  static interface Squeaks2 extends Squeaks {\n");
+    buggyCode.append("    public void squeak2();\n");
+    buggyCode.append("  }\n");
+    buggyCode.append("  static class Squeaker extends JavaScriptObject implements Squeaks2 {\n");
+    buggyCode.append("    public final void squeak() { }\n");
+    buggyCode.append("    public final void squeak2() { }\n");
+    buggyCode.append("    protected Squeaker() { }\n");
+    buggyCode.append("  }\n");
+    buggyCode.append("}\n");
+
+    shouldGenerateError(
+        buggyCode,
+        "Line 11: "
+            + JSORestrictionsChecker.errInterfaceWithMethodsNoAnnotation(("Buggy$Squeaks2")));
+  }
+
+  public void testNoConstructor() {
+    StringBuffer buggyCode = new StringBuffer();
+    buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+    buggyCode.append("public class Buggy extends JavaScriptObject {\n");
+    buggyCode.append("}\n");
+
+    // The public constructor is implicit.
+    shouldGenerateError(buggyCode, "Line 2: "
+        + JSORestrictionsChecker.ERR_NONPROTECTED_CONSTRUCTOR);
   }
 
   public void testNonEmptyConstructor() {
@@ -121,6 +285,32 @@
         + JSORestrictionsChecker.ERR_INSTANCE_METHOD_NONFINAL);
   }
 
+  public void testNonJsoInterfaceExtension() {
+    StringBuffer goodCode = new StringBuffer();
+    goodCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+    goodCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+    goodCode.append("public class Buggy {\n");
+    goodCode.append("  @SingleJsoImpl\n");
+    goodCode.append("  static interface Squeaks {\n");
+    goodCode.append("    public void squeak();\n");
+    goodCode.append("  }\n");
+    goodCode.append("  static interface Squeaks2 extends Squeaks {\n");
+    goodCode.append("    public void squeak2();\n");
+    goodCode.append("  }\n");
+    goodCode.append("  static class JsoSqueaker extends JavaScriptObject implements Squeaks {\n");
+    goodCode.append("    protected JsoSqueaker() {}\n");
+    goodCode.append("    public final void squeak() {}\n");
+    goodCode.append("  }\n");
+    goodCode.append("  static class JavaSqueaker2 implements Squeaks2 {\n");
+    goodCode.append("    protected JavaSqueaker2() {}\n");
+    goodCode.append("    public void squeak() {}\n");
+    goodCode.append("    public void squeak2() {}\n");
+    goodCode.append("  }\n");
+    goodCode.append("}\n");
+
+    shouldGenerateNoError(goodCode);
+  }
+
   public void testNonProtectedConstructor() {
     StringBuffer buggyCode = new StringBuffer();
     buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
@@ -168,18 +358,48 @@
     shouldGenerateNoError(code);
   }
 
+  public void testTagInterfaces() {
+    StringBuffer goodCode = new StringBuffer();
+    goodCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+    goodCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+    goodCode.append("public class Buggy {\n");
+    goodCode.append("  static interface Tag {}\n");
+    goodCode.append("  static interface Tag2 extends Tag {}\n");
+    goodCode.append("  @SingleJsoImpl \n");
+    goodCode.append("  static interface IntrExtendsTag extends Tag2 {\n");
+    goodCode.append("    public void intrExtendsTag();\n");
+    goodCode.append("  }\n");
+    goodCode.append("  static class Squeaker3 extends JavaScriptObject implements Tag {\n");
+    goodCode.append("    public final void squeak() { }\n");
+    goodCode.append("    protected Squeaker3() { }\n");
+    goodCode.append("  }\n");
+    goodCode.append("  static class Squeaker4 extends JavaScriptObject implements Tag2 {\n");
+    goodCode.append("    public final void squeak() { }\n");
+    goodCode.append("    protected Squeaker4() { }\n");
+    goodCode.append("  }\n");
+    goodCode.append("  static class Squeaker5 extends JavaScriptObject implements IntrExtendsTag {\n");
+    goodCode.append("    public final void intrExtendsTag() { }\n");
+    goodCode.append("    protected Squeaker5() { }\n");
+    goodCode.append("  }\n");
+    goodCode.append("}\n");
+
+    shouldGenerateNoError(goodCode);
+  }
+
   /**
    * Test that when compiling buggyCode, the TypeOracleBuilder emits
    * expectedError somewhere in its output. The code should define a class named
    * Buggy.
    */
   private void shouldGenerateError(CharSequence buggyCode,
-      final String expectedError) {
+      String... expectedErrors) {
     UnitTestTreeLogger.Builder builder = new UnitTestTreeLogger.Builder();
     builder.setLowestLogLevel(TreeLogger.ERROR);
-    if (expectedError != null) {
+    if (expectedErrors != null) {
       builder.expectError("Errors in \'/mock/Buggy\'", null);
-      builder.expectError(expectedError, null);
+      for (String e : expectedErrors) {
+        builder.expectError(e, null);
+      }
     }
     UnitTestTreeLogger logger = builder.createLogger();
     CompilationUnit buggyCup = new MockCompilationUnit("Buggy",
@@ -189,6 +409,6 @@
   }
 
   private void shouldGenerateNoError(StringBuffer buggyCode) {
-    shouldGenerateError(buggyCode, null);
+    shouldGenerateError(buggyCode, (String[]) null);
   }
 }
diff --git a/dev/core/test/com/google/gwt/dev/javac/TypeOracleTestingUtils.java b/dev/core/test/com/google/gwt/dev/javac/TypeOracleTestingUtils.java
index 48c34b3..20468c7 100644
--- a/dev/core/test/com/google/gwt/dev/javac/TypeOracleTestingUtils.java
+++ b/dev/core/test/com/google/gwt/dev/javac/TypeOracleTestingUtils.java
@@ -55,7 +55,8 @@
         validBinaryTypeNames.add(compiledClass.getBinaryName());
       }
     }
-    CompilationUnitInvalidator.validateCompilationUnits(units,
+    CompilationUnitInvalidator.InvalidatorState state = new CompilationUnitInvalidator.InvalidatorState();
+    CompilationUnitInvalidator.validateCompilationUnits(state, units,
         validBinaryTypeNames);
     if (CompilationUnitInvalidator.invalidateUnitsWithErrors(logger, units)) {
       CompilationUnitInvalidator.invalidateUnitsWithInvalidRefs(logger, units);
diff --git a/distro-source/core/src/doc/helpInfo/jsoRestrictions.html b/distro-source/core/src/doc/helpInfo/jsoRestrictions.html
index aed2d81..0354e56 100644
--- a/distro-source/core/src/doc/helpInfo/jsoRestrictions.html
+++ b/distro-source/core/src/doc/helpInfo/jsoRestrictions.html
@@ -4,50 +4,58 @@
 <title>Restrictions on subclasses of JavaScriptObject</title>
 </head>
 <body>
-
-<p>Subclasses of JavaScriptObject  represent a view of a JavaScript object
-from Java.  Such classes must conform to a number of restrictions so
-that the compiler can implement them.  This page lists those restrictions.
-
-<p>In the following,  "JSO class" means any subclass of
-<code>JavaScriptObject</code>.  Rationales are written <em>like
-this</em>.
-
+<p>Subclasses of JavaScriptObject represent a view of a JavaScript
+object from Java. Such classes must conform to a number of restrictions
+so that the compiler can implement them. This page lists those
+restrictions.</p>
+<p>In the following, "JSO class" means any subclass of <code>JavaScriptObject</code>.
+Rationales are written <em>like this</em>.</p>
 <ol>
-
-  <li>  All instance methods on JSO classes must be one of: explicitly
-  final, a member of a final class, or private.  <em>Methods of JSO classes
-  cannot be overridden, because calls to such methods could require
-  dynamic dispatch.</em>
-
-  <li> JSO classes cannot implement interfaces that define
-  methods. <em>This prevents virtual calls that would arise by
-  upcasting to the interface and then calling through the interface.
-  The programmer should instead use a wrapper, for example using
-  <code>Comparator</code> instead of implementing
-  <code>Comparable</code>.</em>
-
-  <li> No instance methods on JSO classes may override another
-  method. <em>This catches accidents where JSO itself did not finalize
-  some method from its superclass.</em>
-
-  <li> JSO classes cannot have instance fields.  <em>The fields would
-  have no place to live in web mode.  Programmers should instead make
-  an explicit wrapper class and put the fields there.</em>
-
-  <li> Nested JSO classes must be static.  <em>The implicit
-  <code>this</code> fields of a non-static inner class has the same
-  problems as an explicit field.</em>
-
-  <li> "new" operations cannot be used with JSO classes.  <em>This
-  avoids ever being able to try to instantiate JSO objects using the
-  new keyword. New JSO instances can only come from JSNI, as in
-  previous versions of GWT.</em>
-
-  <li> Every JSO class must have precisely one constructor, and it must
-  be protected, empty, and no-argument.
-
+  <li>All instance methods on JSO classes must be one of:
+  explicitly final, a member of a final class, or private. <em>Methods
+  of JSO classes cannot be overridden, because calls to such methods
+  could require dynamic dispatch.</em></li>
+  <li>JSO classes may implement interfaces that define methods, but
+  only if those interfaces are annotated with <code>@SingleJsoImpl</code>.
+  <em>This ensures that polymorphic dispatch via a <code>@SingleJsoImpl</code>
+  interface can be statically resolved to exactly one implementing JSO
+  subtype.</em>
+  <ol>
+    <li>A <code>@SingleJsoImpl</code> interface may only extend
+    "tag" interfaces that declare no methods or other interfaces
+    annotated with <code>@SingleJsoImpl</code>.</li>
+    <li>It is valid for a <code>@SingleJsoImpl</code> interface to
+    be extended by an interface that is not annotated in this fashion,
+    however the sub-interface may not be implemented by any <code>JavaScriptObjects</code>.</li>
+    <li>A <code>JavaScriptObject</code> that implements a <code>@SingleJsoImpl</code>
+    interface may be further extended. The subclasses may implement
+    additional <code>@SingleJsoImpl</code> interfaces. <em>The
+    methods on a JSO must be effectively final, so each <code>@SingleJsoImpl</code>
+    method still has a 1:1 mapping to a method defined within a JSO
+    subtype.</em></li>
+    <li>It is valid for any number of any non-<code>JavaScriptObject</code>
+    types to implement a <code>@SingleJsoImpl</code> interface. <em>There
+    is a runtime dispatch penalty when a <code>@SingleJsoImpl</code>
+    interface is implemented by both JSO and non-JSO types.</em></li>
+  </ol>
+  </li>
+  <li>No instance methods on JSO classes may override another
+  method. <em>This catches accidents where JSO itself did not
+  finalize some method from its superclass.</em></li>
+  <li>JSO classes cannot have instance fields. <em>The fields
+  would have no place to live in web mode. Programmers should instead
+  make an explicit wrapper class and put the fields there.</em></li>
+  <li>Nested JSO classes must be static. <em>The implicit <code>this</code>
+  fields of a non-static inner class has the same problems as an
+  explicit field.</em></li>
+  <li>"new" operations cannot be used with JSO classes. <em>This
+  avoids ever being able to try to instantiate JSO objects using the new
+  keyword. New JSO instances can only come from JSNI, as in previous
+  versions of GWT.</em></li>
+  <li>JSNI methods may not refer to instance methods defined within
+  a <code>JavaScriptObject</code>.</li>
+  <li>Every JSO class must have precisely one constructor, and it
+  must be protected, empty, and no-argument.</li>
 </ol>
-
 </body>
 </html>
diff --git a/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java b/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
index b4ed8af..7058dd5 100644
--- a/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
+++ b/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
@@ -166,6 +166,8 @@
     sw.println();
     sw.println("protected final GWTTestCase createNewTestCase(String testClass) {");
     sw.indent();
+    sw.println("try {");
+    sw.indent();
     boolean isFirst = true;
     for (String className : testClasses) {
       if (isFirst) {
@@ -178,6 +180,11 @@
       sw.indentln("return GWT.create(" + className + ".class);");
       sw.println("}");
     }
+    sw.outdent();
+    sw.println("} catch (Throwable t) {");
+    sw.indentln("// Crash in a useful manner");
+    sw.indentln("GWT.log(\"Unable to construct TestCase: \" + testClass, t);");
+    sw.println("}");
     sw.println("return null;");
     sw.outdent();
     sw.println("}");
diff --git a/user/test/com/google/gwt/dev/jjs/CompilerSuite.java b/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
index fea18ff..9f0c603 100644
--- a/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
+++ b/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
@@ -40,6 +40,7 @@
 import com.google.gwt.dev.jjs.test.NativeLongTest;
 import com.google.gwt.dev.jjs.test.ObjectIdentityTest;
 import com.google.gwt.dev.jjs.test.RunAsyncTest;
+import com.google.gwt.dev.jjs.test.SingleJsoImplTest;
 import com.google.gwt.dev.jjs.test.UnstableGeneratorTest;
 import com.google.gwt.dev.jjs.test.VarargsTest;
 import com.google.gwt.junit.tools.GWTTestSuite;
@@ -80,6 +81,7 @@
     suite.addTestSuite(NativeLongTest.class);
     suite.addTestSuite(ObjectIdentityTest.class);
     suite.addTestSuite(RunAsyncTest.class);
+    suite.addTestSuite(SingleJsoImplTest.class);
     suite.addTestSuite(UnstableGeneratorTest.class);
     suite.addTestSuite(VarargsTest.class);
     // $JUnit-END$
diff --git a/user/test/com/google/gwt/dev/jjs/test/SingleJsoImplTest.java b/user/test/com/google/gwt/dev/jjs/test/SingleJsoImplTest.java
new file mode 100644
index 0000000..0914f54
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/test/SingleJsoImplTest.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.jjs.test;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.SingleJsoImpl;
+import com.google.gwt.junit.client.GWTTestCase;
+
+import java.io.IOException;
+
+/**
+ * Ensures that JavaScriptObjects may implement interfaces with methods.
+ */
+public class SingleJsoImplTest extends GWTTestCase {
+
+  @SingleJsoImpl
+  interface Adder {
+    int ADDER_CONST = 6;
+
+    /**
+     * @see SameDescriptors#SAME_NAME
+     */
+    String SAME_NAME = "Same Name";
+
+    Object DIFFERENT_OBJECT = new Object();
+    Object SAME_OBJECT = new Object();
+    Object SAME_OBJECT2 = SAME_OBJECT;
+
+    /*
+     * NB: Picking a mix of double and int because doubles have size 2 on the
+     * stack, so this ensures that we're handling that case correctly.
+     */
+    double add(double a, int b);
+  }
+
+  @SingleJsoImpl
+  interface Divider extends Multiplier {
+    int divide(int a, int b);
+  }
+
+  static class JavaAdder implements Adder {
+    public double add(double a, int b) {
+      return a + b;
+    }
+  }
+
+  /**
+   * The extra declaration of implementing Multiplier should still be legal.
+   */
+  static class JavaDivider extends JavaMultiplier implements Divider,
+      Multiplier {
+    public int divide(int a, int b) {
+      return a / b;
+    }
+  }
+
+  /**
+   * Ensure that SingleJsoImpl interfaces can be extended and implemented by
+   * regular Java types.
+   */
+  static class JavaLog2 extends JavaDivider implements Log2 {
+    public double log2(int a) {
+      return Math.log(a) / Math.log(2);
+    }
+  }
+
+  static class JavaMultiplier implements Multiplier {
+    public int multiply(int a, int b) {
+      return a * b;
+    }
+  }
+
+  static class JsoAdder extends JavaScriptObject implements Adder {
+    protected JsoAdder() {
+    }
+
+    public final native double add(double a, int b) /*-{return this.offset * (a + b);}-*/;
+  }
+
+  static class JsoDivider extends JsoMultiplier implements Divider {
+    protected JsoDivider() {
+    }
+
+    public final native int divide(int a, int b) /*-{return this.offset * a / b;}-*/;
+  }
+
+  static class JsoMultiplier extends JavaScriptObject implements Multiplier {
+    protected JsoMultiplier() {
+    }
+
+    public final native int multiply(int a, int b) /*-{return this.offset * a * b;}-*/;
+  }
+
+  /**
+   * Just a random JSO type for testing cross-casting.
+   */
+  final static class JsoRandom extends JavaScriptObject implements
+      SameDescriptors {
+    protected JsoRandom() {
+    }
+
+    public int add(int a, int b) {
+      return -1;
+    }
+
+    public int divide(int a, int b) {
+      return -1;
+    }
+
+    public int multiply(int a, int b) {
+      return -1;
+    }
+
+    String b() {
+      return "b";
+    }
+  }
+
+  final static class JsoSimple extends JavaScriptObject implements Simple {
+    protected JsoSimple() {
+    }
+
+    public String a() {
+      return "a";
+    }
+
+    public String ex() throws IOException {
+      throw new IOException();
+    }
+
+    public String rte() {
+      throw new IllegalArgumentException();
+    }
+  }
+
+  /**
+   * Ensure that SingleJsoImpl interfaces can be extended and implemented by
+   * regular Java types.
+   */
+  interface Log2 extends Divider {
+    double log2(int a);
+  }
+
+  @SingleJsoImpl
+  interface Multiplier {
+    int multiply(int a, int b);
+  }
+
+  /**
+   * This interface makes sure that types with identical method signatures will
+   * still dispatch correctly.
+   */
+  @SingleJsoImpl
+  interface SameDescriptors {
+    int SAME_NAME = 6;
+
+    int add(int a, int b);
+
+    int divide(int a, int b);
+
+    int multiply(int a, int b);
+  }
+
+  @SingleJsoImpl
+  interface Simple {
+    String a();
+
+    String ex() throws IOException;
+
+    String rte();
+  }
+
+  /**
+   * Ensure that a Java-only implementation of a SingleJsoImpl interface works.
+   */
+  static class SimpleOnlyJava implements SimpleOnlyJavaInterface {
+    public String simpleOnlyJava() {
+      return "simpleOnlyJava";
+    }
+  }
+
+  @SingleJsoImpl
+  interface SimpleOnlyJavaInterface {
+    String simpleOnlyJava();
+  }
+
+  private static native JsoAdder makeAdder(int offset) /*-{return {offset:offset};}-*/;
+
+  private static native JsoDivider makeDivider(int offset) /*-{return {offset:offset};}-*/;
+
+  private static native JsoMultiplier makeMultiplier(int offset) /*-{return {offset:offset};}-*/;
+
+  private static native JsoSimple makeSimple() /*-{return {};}-*/;
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.dev.jjs.CompilerSuite";
+  }
+
+  public void testDualCase() {
+    // Direct dispatch
+    {
+      JavaAdder java = new JavaAdder();
+      assertEquals(2.0, java.add(1, 1));
+
+      JsoAdder jso = makeAdder(2);
+      assertEquals(4.0, jso.add(1, 1));
+    }
+
+    // Just check dispatch via the interface
+    {
+      Adder a = new JavaAdder();
+      assertEquals(2.0, a.add(1, 1));
+
+      a = makeAdder(2);
+      assertEquals(4.0, a.add(1, 1));
+    }
+
+    // Check casting
+    {
+      Object a = new JavaAdder();
+      assertEquals(2.0, ((Adder) a).add(1, 1));
+      assertEquals(2.0, ((JavaAdder) a).add(1, 1));
+      assertTrue(a instanceof JavaAdder);
+      assertFalse(a instanceof JsoAdder);
+      try {
+        ((JsoAdder) a).add(1, 1);
+        fail("Should have thrown CCE");
+      } catch (ClassCastException e) {
+        // OK
+      }
+
+      a = makeAdder(2);
+      assertEquals(4.0, ((Adder) a).add(1, 1));
+      assertEquals(4.0, ((JsoAdder) a).add(1, 1));
+      assertTrue(a instanceof JsoAdder);
+      assertFalse(a instanceof JavaAdder);
+      try {
+        ((JavaAdder) a).add(1, 1);
+        fail("Should have thrown CCE");
+      } catch (ClassCastException e) {
+        // OK
+      }
+    }
+  }
+
+  public void testFields() {
+    assertEquals(6, Adder.ADDER_CONST);
+    assertEquals("Same Name", Adder.SAME_NAME);
+    assertEquals(6, SameDescriptors.SAME_NAME);
+    assertSame(Adder.SAME_OBJECT, Adder.SAME_OBJECT2);
+    assertNotSame(Adder.DIFFERENT_OBJECT, Adder.SAME_OBJECT);
+  }
+
+  @SuppressWarnings("cast")
+  public void testSimpleCase() {
+    {
+      JsoSimple asJso = makeSimple();
+      assertTrue(asJso instanceof Object);
+      assertTrue(asJso instanceof Simple);
+      assertEquals("a", asJso.a());
+      try {
+        asJso.ex();
+        fail("Should have thrown IOException");
+      } catch (IOException e) {
+        // OK
+      }
+      try {
+        asJso.rte();
+        fail("Should have thrown IllegalArgumentException");
+      } catch (IllegalArgumentException e) {
+        // OK
+      }
+    }
+
+    {
+      Simple asSimple = makeSimple();
+      assertTrue(asSimple instanceof Object);
+      assertTrue(asSimple instanceof JavaScriptObject);
+      assertTrue(asSimple instanceof JsoSimple);
+      assertEquals("a", asSimple.a());
+      assertEquals("a", ((JsoSimple) asSimple).a());
+      try {
+        asSimple.ex();
+        fail("Should have thrown IOException");
+      } catch (IOException e) {
+        // OK
+      }
+      try {
+        asSimple.rte();
+        fail("Should have thrown IllegalArgumentException");
+      } catch (IllegalArgumentException e) {
+        // OK
+      }
+    }
+
+    {
+      Object asObject = "Defeat type-tightening";
+      assertTrue(asObject instanceof String);
+
+      asObject = makeSimple();
+      assertTrue(asObject instanceof Object);
+      assertTrue(asObject instanceof JavaScriptObject);
+      assertTrue(asObject instanceof JsoSimple);
+      assertTrue(asObject instanceof Simple);
+      assertEquals("a", ((Simple) asObject).a());
+      assertEquals("a", ((JsoSimple) asObject).a());
+
+      // Test a cross-cast that's normally not allowed by the type system
+      assertTrue(asObject instanceof JsoRandom);
+      assertEquals("b", ((JsoRandom) asObject).b());
+      assertEquals(-1, ((JsoRandom) asObject).add(1, 1));
+    }
+
+    {
+      Object o = "Defeat type-tightening";
+      assertTrue(o instanceof String);
+      o = new SimpleOnlyJava();
+      assertTrue(o instanceof SimpleOnlyJavaInterface);
+      assertEquals("simpleOnlyJava", ((SimpleOnlyJava) o).simpleOnlyJava());
+    }
+  }
+
+  @SuppressWarnings("cast")
+  public void testSubclassing() {
+    {
+      JsoDivider d = makeDivider(1);
+      assertTrue(d instanceof Divider);
+      assertTrue(d instanceof Multiplier);
+      assertEquals(5, d.divide(10, 2));
+      assertEquals(10, d.multiply(5, 2));
+      assertEquals(10, ((JsoMultiplier) d).multiply(5, 2));
+    }
+
+    {
+      Object d = makeDivider(1);
+      assertTrue(d instanceof Divider);
+      assertTrue(d instanceof Multiplier);
+      assertTrue(d instanceof JsoDivider);
+      assertTrue(d instanceof JsoMultiplier);
+      assertFalse(d instanceof JavaDivider);
+      assertFalse(d instanceof JavaMultiplier);
+
+      assertEquals(5, ((Divider) d).divide(10, 2));
+      assertEquals(10, ((Divider) d).multiply(5, 2));
+      assertEquals(10, ((Multiplier) d).multiply(5, 2));
+      assertEquals(5, ((JsoDivider) d).divide(10, 2));
+      assertEquals(10, ((JsoMultiplier) d).multiply(5, 2));
+
+      d = new JavaDivider();
+      assertTrue(d instanceof Divider);
+      assertTrue(d instanceof Multiplier);
+      assertTrue(d instanceof JavaDivider);
+      assertTrue(d instanceof JavaMultiplier);
+      assertFalse(d instanceof JsoDivider);
+      assertFalse(d instanceof JsoMultiplier);
+
+      assertEquals(5, ((Divider) d).divide(10, 2));
+      assertEquals(10, ((Divider) d).multiply(5, 2));
+      assertEquals(10, ((Multiplier) d).multiply(5, 2));
+      assertEquals(5, ((JavaDivider) d).divide(10, 2));
+      assertEquals(10, ((JavaMultiplier) d).multiply(5, 2));
+    }
+
+    {
+      Object m = makeMultiplier(1);
+      // This only works because JSO$ implements every SingleJsoImpl interface
+      assertTrue(m instanceof Divider);
+      assertEquals(2, ((Divider) m).divide(10, 5));
+
+      m = new JavaMultiplier();
+      // but this should fail, since there's proper type information
+      assertFalse(m instanceof Divider);
+      try {
+        assertEquals(2, ((Divider) m).divide(10, 5));
+        fail("Should have thrown CCE");
+      } catch (ClassCastException e) {
+        // OK
+      }
+    }
+
+    {
+      Object l2 = "Prevent type-tightening";
+      assertTrue(l2 instanceof String);
+
+      l2 = new JavaLog2();
+      assertTrue(l2 instanceof Log2);
+      assertTrue(l2 instanceof Multiplier);
+      assertTrue(l2 instanceof Divider);
+      assertEquals(4.0, ((Log2) l2).log2(16));
+    }
+  }
+}