Change the way code-splitting and string interning is performed on the JS AST to allow new strings to be introduced by JsVisitors (such as JsStackEmulator).

Patch by: bobv, spoon (JProgram)
Review by: spoon, bobv

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5812 8db76d5a-ed1c-0410-87a9-c151d255dfc7
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 d99c393..874fc6f 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -42,14 +42,12 @@
 import com.google.gwt.dev.jjs.ast.JClassType;
 import com.google.gwt.dev.jjs.ast.JDeclaredType;
 import com.google.gwt.dev.jjs.ast.JExpression;
-import com.google.gwt.dev.jjs.ast.JField;
 import com.google.gwt.dev.jjs.ast.JGwtCreate;
 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.JProgram;
 import com.google.gwt.dev.jjs.ast.JReboundEntryPoint;
-import com.google.gwt.dev.jjs.ast.JReferenceType;
 import com.google.gwt.dev.jjs.ast.JStatement;
 import com.google.gwt.dev.jjs.impl.ArrayNormalizer;
 import com.google.gwt.dev.jjs.impl.AssertionNormalizer;
@@ -99,7 +97,6 @@
 import com.google.gwt.dev.js.JsVerboseNamer;
 import com.google.gwt.dev.js.ast.JsName;
 import com.google.gwt.dev.js.ast.JsProgram;
-import com.google.gwt.dev.js.ast.JsStatement;
 import com.google.gwt.dev.util.AbstractTextOutput;
 import com.google.gwt.dev.util.DefaultTextOutput;
 import com.google.gwt.dev.util.Empty;
@@ -120,7 +117,6 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -140,7 +136,7 @@
     private final byte[] serializedSymbolMap;
     private final StatementRanges[] statementRanges;
     private final int permutationId;
-    
+
     public PermutationResultImpl(String[] js, SymbolData[] symbolMap,
         StatementRanges[] statementRanges, int permutationId) {
       byte[][] bytes = new byte[js.length][];
@@ -167,7 +163,7 @@
     public byte[][] getJs() {
       return js;
     }
-    
+
     public int getPermutationId() {
       return permutationId;
     }
@@ -270,34 +266,11 @@
         } while (didChange);
       }
 
-      // (10) Obfuscate
-      final Map<JsName, String> stringLiteralMap;
-      switch (options.getOutput()) {
-        case OBFUSCATED:
-          stringLiteralMap = JsStringInterner.exec(jsProgram);
-          JsObfuscateNamer.exec(jsProgram);
-          break;
-        case PRETTY:
-          // We don't intern strings in pretty mode to improve readability
-          stringLiteralMap = new HashMap<JsName, String>();
-          JsPrettyNamer.exec(jsProgram);
-          break;
-        case DETAILED:
-          stringLiteralMap = JsStringInterner.exec(jsProgram);
-          JsVerboseNamer.exec(jsProgram);
-          break;
-        default:
-          throw new InternalCompilerException("Unknown output mode");
-      }
-
-      JavaToJavaScriptMap postStringInterningMap = addStringLiteralMap(map,
-          stringLiteralMap);
-
-      // (10.5) Split up the program into fragments
+      // (10) Split up the program into fragments
       SoycArtifact dependencies = null;
       if (options.isAggressivelyOptimize() && options.isRunAsyncEnabled()) {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        CodeSplitter.exec(logger, jprogram, jsProgram, postStringInterningMap,
+        CodeSplitter.exec(logger, jprogram, jsProgram, map,
             chooseDependencyRecorder(options.isSoycEnabled(), baos));
         if (baos.size() > 0) {
           dependencies = new SoycArtifact("dependencies" + permutationId
@@ -305,6 +278,24 @@
         }
       }
 
+      // (10.5) Obfuscate
+      switch (options.getOutput()) {
+        case OBFUSCATED:
+          JsStringInterner.exec(jprogram, jsProgram);
+          JsObfuscateNamer.exec(jsProgram);
+          break;
+        case PRETTY:
+          // We don't intern strings in pretty mode to improve readability
+          JsPrettyNamer.exec(jsProgram);
+          break;
+        case DETAILED:
+          JsStringInterner.exec(jprogram, jsProgram);
+          JsVerboseNamer.exec(jsProgram);
+          break;
+        default:
+          throw new InternalCompilerException("Unknown output mode");
+      }
+
       // (11) Perform any post-obfuscation normalizations.
 
       // Work around an IE7 bug,
@@ -340,7 +331,8 @@
           makeSymbolMap(symbolTable), ranges, permutationId);
 
       toReturn.getArtifacts().add(
-          makeSoycArtifact(logger, permutationId, jprogram, js, sourceInfoMaps, dependencies));
+          makeSoycArtifact(logger, permutationId, jprogram, js, sourceInfoMaps,
+              dependencies));
 
       System.out.println("Permutation took "
           + (System.currentTimeMillis() - permStart) + " ms");
@@ -622,40 +614,6 @@
     return didChange;
   }
 
-  private static JavaToJavaScriptMap addStringLiteralMap(
-      final JavaToJavaScriptMap map, final Map<JsName, String> stringLiteralMap) {
-    JavaToJavaScriptMap postStringInterningMap = new JavaToJavaScriptMap() {
-      public JsName nameForMethod(JMethod method) {
-        return map.nameForMethod(method);
-      }
-
-      public JsName nameForType(JReferenceType type) {
-        return map.nameForType(type);
-      }
-
-      public JField nameToField(JsName name) {
-        return map.nameToField(name);
-      }
-
-      public JMethod nameToMethod(JsName name) {
-        return map.nameToMethod(name);
-      }
-
-      public String stringLiteralForName(JsName name) {
-        return stringLiteralMap.get(name);
-      }
-
-      public JReferenceType typeForStatement(JsStatement stat) {
-        return map.typeForStatement(stat);
-      }
-
-      public JMethod vtableInitToMethod(JsStatement stat) {
-        return map.vtableInitToMethod(stat);
-      }
-    };
-    return postStringInterningMap;
-  }
-
   private static void checkForErrors(TreeLogger logger,
       CompilationUnitDeclaration[] cuds, boolean itemizeErrors)
       throws UnableToCompleteException {
diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java
index 38e7411..6ba32ab 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java
@@ -24,6 +24,7 @@
 import com.google.gwt.dev.jjs.ast.js.JClassSeed;
 import com.google.gwt.dev.jjs.ast.js.JsniMethodBody;
 import com.google.gwt.dev.jjs.ast.js.JsonObject;
+import com.google.gwt.dev.jjs.impl.CodeSplitter;
 import com.google.gwt.dev.jjs.impl.ReplaceRunAsyncs.RunAsyncReplacement;
 import com.google.gwt.dev.util.collect.Lists;
 import com.google.gwt.dev.util.collect.Maps;
@@ -149,6 +150,26 @@
     return traceMethods.size() > 0;
   }
 
+  /**
+   * The same as {@link #lastFragmentLoadingBefore(int, int...)}, except that
+   * all of the parameters must be passed explicitly. The instance method should
+   * be preferred whenever a JProgram instance is available.
+   * 
+   * @param initialSeq The initial split point sequence of the program
+   * @param numSps The number of split points in the program
+   * @param firstFragment The first fragment to consider
+   * @param restFragments The rest of the fragments to consider
+   */
+  public static int lastFragmentLoadingBefore(List<Integer> initialSeq,
+      int numSps, int firstFragment, int... restFragments) {
+    int latest = firstFragment;
+    for (int frag : restFragments) {
+      latest = pairwiseLastFragmentLoadingBefore(initialSeq, numSps, latest,
+          frag);
+    }
+    return latest;
+  }
+
   private static String dotify(char[][] name) {
     StringBuffer result = new StringBuffer();
     for (int i = 0; i < name.length; ++i) {
@@ -161,6 +182,51 @@
     return result.toString();
   }
 
+  /**
+   * The main logic behind {@link #lastFragmentLoadingBefore(int, int...)} and
+   * {@link #lastFragmentLoadingBefore(List, int, int, int...)}.
+   */
+  private static int pairwiseLastFragmentLoadingBefore(
+      List<Integer> initialSeq, int numSps, int frag1, int frag2) {
+    if (frag1 == frag2) {
+      return frag1;
+    }
+
+    if (frag1 == 0) {
+      return 0;
+    }
+
+    if (frag2 == 0) {
+      return 0;
+    }
+
+    // See if either is in the initial sequence
+    int initPos1 = initialSeq.indexOf(frag1);
+    int initPos2 = initialSeq.indexOf(frag2);
+
+    // If both are in the initial sequence, then pick the earlier
+    if (initPos1 >= 0 && initPos2 >= 0) {
+      if (initPos1 < initPos2) {
+        return frag1;
+      }
+      return frag2;
+    }
+
+    // If exactly one is in the initial sequence, then it's the earlier one
+    if (initPos1 >= 0) {
+      return frag1;
+    }
+    if (initPos2 >= 0) {
+      return frag2;
+    }
+
+    assert (initPos1 < 0 && initPos2 < 0);
+    assert (frag1 != frag2);
+
+    // They are both leftovers or exclusive. Leftovers goes first in all cases.
+    return CodeSplitter.getLeftoversFragmentNumber(numSps);
+  }
+
   public final List<JClassType> codeGenTypes = new ArrayList<JClassType>();
 
   /**
@@ -1026,6 +1092,16 @@
     return staticToInstanceMap.containsKey(method);
   }
 
+  /**
+   * Given a sequence of fragment numbers, return the latest fragment number
+   * possible that does not load later than any of these. It might be one of the
+   * supplied fragments, or it might be a common predecessor.
+   */
+  public int lastFragmentLoadingBefore(int firstFragment, int... restFragments) {
+    return lastFragmentLoadingBefore(splitPointInitialSequence,
+        entryMethods.size() - 1, firstFragment, restFragments);
+  }
+
   public void putIntoTypeMap(String qualifiedBinaryName, JDeclaredType type) {
     // Make it into a source type name.
     String srcTypeName = qualifiedBinaryName.replace('$', '.');
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/CodeSplitter.java b/dev/core/src/com/google/gwt/dev/jjs/impl/CodeSplitter.java
index 22bd0ce..214008f 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/CodeSplitter.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/CodeSplitter.java
@@ -150,10 +150,6 @@
             if (field != null) {
               System.out.println(fullNameString(field));
             }
-            String string = map.stringLiteralForName(var.getName());
-            if (string != null) {
-              System.out.println("STRING " + var.getName());
-            }
           }
         }
       }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentExtractor.java b/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentExtractor.java
index 0bfedd0..864be36 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentExtractor.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentExtractor.java
@@ -98,8 +98,8 @@
    * 
    * <li>Instance methods depend on their enclosing type.</li>
    * 
-   * <li>Static fields that are initialized to strings depend on the string
-   * they are initialized to.</li>
+   * <li>Static fields that are initialized to strings depend on the string they
+   * are initialized to.</li>
    * </ul>
    */
   public static interface LivenessPredicate {
@@ -115,8 +115,8 @@
      * Whether miscellelaneous statements should be considered live.
      * Miscellaneous statements are any that the fragment extractor does not
      * recognize as being in any particular category. This method should almost
-     * always return <code>true</code>, but does return <code>false</code>
-     * for {@link NothingAlivePredicate}.
+     * always return <code>true</code>, but does return <code>false</code> for
+     * {@link NothingAlivePredicate}.
      */
     boolean miscellaneousStatementsAreLive();
   }
@@ -149,7 +149,8 @@
   /**
    * A logger for statements that the fragment extractor encounters. Install one
    * using
-   * {@link FragmentExtractor#setStatementLogger(com.google.gwt.fragserv.FragmentExtractor.StatementLogger)}.
+   * {@link FragmentExtractor#setStatementLogger(com.google.gwt.fragserv.FragmentExtractor.StatementLogger)}
+   * .
    */
   public static interface StatementLogger {
     void logStatement(JsStatement stat, boolean isIncluded);
@@ -204,7 +205,8 @@
 
   /**
    * Create a call to
-   * {@link com.google.gwt.core.client.impl.AsyncFragmentLoader#leftoversFragmentHasLoaded()}.
+   * {@link com.google.gwt.core.client.impl.AsyncFragmentLoader#leftoversFragmentHasLoaded()}
+   * .
    */
   public List<JsStatement> createCallToLeftoversFragmentHasLoaded() {
     JMethod loadedMethod = jprogram.getIndexedMethod("AsyncFragmentLoader.browserLoaderLeftoversFragmentHasLoaded");
@@ -315,16 +317,12 @@
    * individual vars that could be removed. If it does, then
    * {@link #removeSomeVars(JsVars, LivenessPredicate, LivenessPredicate)} is
    * sensible for this statement and should be used instead of
-   * {@link #isLive(JsStatement, com.google.gwt.fragserv.FragmentExtractor.LivenessPredicate)}.
+   * {@link #isLive(JsStatement, com.google.gwt.fragserv.FragmentExtractor.LivenessPredicate)}
+   * .
    */
   private boolean containsRemovableVars(JsStatement stat) {
     if (stat instanceof JsVars) {
       for (JsVar var : (JsVars) stat) {
-        String lit = map.stringLiteralForName(var.getName());
-        if (lit != null) {
-          // It's an intern variable for a string literal
-          return true;
-        }
 
         JField field = map.nameToField(var.getName());
         if (field != null) {
@@ -396,12 +394,6 @@
    * {@link #containsRemovableVars(JsStatement)}.
    */
   private boolean isLive(JsVar var, LivenessPredicate livenessPredicate) {
-    String lit = map.stringLiteralForName(var.getName());
-    if (lit != null) {
-      // It's an intern variable for a string literal
-      return livenessPredicate.isLive(lit);
-    }
-
     JField field = map.nameToField(var.getName());
     if (field != null) {
       // It's a field
@@ -414,9 +406,9 @@
 
   /**
    * Return the Java method corresponding to <code>stat</code>, or
-   * <code>null</code> if there isn't one. It recognizes JavaScript of the
-   * form <code>function foo(...) { ...}</code>, where <code>foo</code> is
-   * the name of the JavaScript translation of a Java method.
+   * <code>null</code> if there isn't one. It recognizes JavaScript of the form
+   * <code>function foo(...) { ...}</code>, where <code>foo</code> is the name
+   * of the JavaScript translation of a Java method.
    */
   private JMethod methodFor(JsStatement stat) {
     if (stat instanceof JsExprStmt) {
@@ -472,8 +464,8 @@
   /**
    * Compute a statement that can be used to set up for installing instance
    * methods into a vtable. It will be of the form
-   * <code>_ = foo.prototype</code>, where <code>foo</code> is the
-   * constructor function for <code>vtableType</code>.
+   * <code>_ = foo.prototype</code>, where <code>foo</code> is the constructor
+   * function for <code>vtableType</code>.
    */
   private JsStatement vtableStatFor(JReferenceType vtableType) {
     JsNameRef prototypeField = new JsNameRef(
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
index b8f03dc..d56c411 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
@@ -2133,14 +2133,6 @@
         return nameToMethodMap.get(name);
       }
 
-      public String stringLiteralForName(JsName var) {
-        /*
-         * This method will be supplied non-trivially elsewhere.
-         * GenerateJavaScriptAST doesn't have the information to implement it.
-         */
-        return null;
-      }
-
       public JReferenceType typeForStatement(JsStatement stat) {
         return typeForStatMap.get(stat);
       }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JavaToJavaScriptMap.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JavaToJavaScriptMap.java
index 996fce0..7a96d82 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JavaToJavaScriptMap.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JavaToJavaScriptMap.java
@@ -49,12 +49,6 @@
   JMethod nameToMethod(JsName name);
 
   /**
-   * If <code>var</code> is the name of the variable used to hold an interned
-   * string literal, then return the string it interns. Otherwise, return null.
-   */
-  String stringLiteralForName(JsName var);
-
-  /**
    * If <code>stat</code> is used to set up the definition of some class, return
    * that class. Otherwise, return null.
    */
diff --git a/dev/core/src/com/google/gwt/dev/js/JsStringInterner.java b/dev/core/src/com/google/gwt/dev/js/JsStringInterner.java
index 4c10370..415737a 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsStringInterner.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsStringInterner.java
@@ -16,6 +16,7 @@
 package com.google.gwt.dev.js;
 
 import com.google.gwt.dev.jjs.SourceInfo;
+import com.google.gwt.dev.jjs.ast.JProgram;
 import com.google.gwt.dev.js.ast.JsBinaryOperation;
 import com.google.gwt.dev.js.ast.JsBlock;
 import com.google.gwt.dev.js.ast.JsContext;
@@ -25,33 +26,48 @@
 import com.google.gwt.dev.js.ast.JsPostfixOperation;
 import com.google.gwt.dev.js.ast.JsPrefixOperation;
 import com.google.gwt.dev.js.ast.JsProgram;
+import com.google.gwt.dev.js.ast.JsProgramFragment;
 import com.google.gwt.dev.js.ast.JsPropertyInitializer;
 import com.google.gwt.dev.js.ast.JsScope;
 import com.google.gwt.dev.js.ast.JsStringLiteral;
 import com.google.gwt.dev.js.ast.JsVars;
 import com.google.gwt.dev.js.ast.JsVars.JsVar;
 
+import java.util.Collection;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.SortedMap;
+import java.util.SortedSet;
 import java.util.TreeMap;
+import java.util.TreeSet;
 
 /**
  * Interns all String literals in a JsProgram. Each unique String will be
- * assigned to a variable in the program's main block and instances of a
- * JsStringLiteral replaced with a JsNameRef. This optimization is complete in a
- * single pass, although it may be performed multiple times without duplicating
- * the intern pool.
+ * assigned to a variable in an appropriate program fragment and the
+ * JsStringLiteral will be replaced with a JsNameRef. This optimization is
+ * complete in a single pass, although it may be performed multiple times
+ * without duplicating the intern pool.
  */
 public class JsStringInterner {
-
   /**
    * Replaces JsStringLiterals with JsNameRefs, creating new JsName allocations
    * on the fly.
    */
   private static class StringVisitor extends JsModVisitor {
     /**
+     * The current fragment being visited.
+     */
+    private int currentFragment = 0;
+
+    /**
+     * This map records which program fragment the variable for this JsName
+     * should be created in.
+     */
+    private final SortedMap<JsStringLiteral, Integer> fragmentAssignment = new TreeMap<JsStringLiteral, Integer>(
+        LITERAL_COMPARATOR);
+
+    /**
      * A counter used for assigning ids to Strings. Even though it's unlikely
      * that someone would actually have two billion strings in their
      * application, it doesn't hurt to think ahead.
@@ -59,7 +75,13 @@
     private long lastId = 0;
 
     /**
-     * Records the scope in which the interned identifiers should be unique.
+     * Only used to get fragment load order so strings used in multiple
+     * fragments need only be downloaded once.
+     */
+    private final JProgram program;
+
+    /**
+     * Records the scope in which the interned identifiers are declared.
      */
     private final JsScope scope;
 
@@ -68,11 +90,7 @@
      * lexicographical ordering of the string constant.
      */
     private final SortedMap<JsStringLiteral, JsName> toCreate = new TreeMap<JsStringLiteral, JsName>(
-        new Comparator<JsStringLiteral>() {
-          public int compare(JsStringLiteral o1, JsStringLiteral o2) {
-            return o1.getValue().compareTo(o2.getValue());
-          }
-        });
+        LITERAL_COMPARATOR);
 
     /**
      * Constructor.
@@ -80,10 +98,16 @@
      * @param scope specifies the scope in which the interned strings should be
      *          created.
      */
-    public StringVisitor(JsScope scope) {
+    public StringVisitor(JProgram program, JsScope scope) {
+      this.program = program;
       this.scope = scope;
     }
 
+    @Override
+    public void endVisit(JsProgramFragment x, JsContext<JsProgramFragment> ctx) {
+      currentFragment++;
+    }
+
     /**
      * Prevents 'fixing' an otherwise illegal operation.
      */
@@ -133,6 +157,24 @@
         toCreate.put(x, name);
       }
 
+      Integer currentAssignment = fragmentAssignment.get(x);
+      if (currentAssignment == null) {
+        // Assign the JsName to the current program fragment
+        fragmentAssignment.put(x, currentFragment);
+
+      } else if (currentAssignment != currentFragment) {
+        // See if we need to move the assignment to a common ancestor
+        assert program != null : "JsStringInterner cannot be used with "
+            + "fragmented JsProgram without an accompanying JProgram";
+
+        int newAssignment = program.lastFragmentLoadingBefore(currentFragment,
+            currentAssignment);
+        if (newAssignment != currentAssignment) {
+          // Assign the JsName to the common ancestor
+          fragmentAssignment.put(x, newAssignment);
+        }
+      }
+
       ctx.replaceMe(name.makeRef(x.getSourceInfo().makeChild(
           JsStringInterner.class, "Interned reference")));
 
@@ -149,9 +191,45 @@
     }
   }
 
+  private static final Comparator<JsStringLiteral> LITERAL_COMPARATOR = new Comparator<JsStringLiteral>() {
+    public int compare(JsStringLiteral o1, JsStringLiteral o2) {
+      return o1.getValue().compareTo(o2.getValue());
+    }
+  };
+
   public static final String PREFIX = "$intern_";
 
   /**
+   * Apply interning of String literals to a JsProgram. The symbol names for the
+   * interned strings will be defined within the program's top scope and the
+   * symbol declarations will be added as the first statement in the program's
+   * global block.
+   * 
+   * @param jprogram the JProgram that has fragment dependency data for
+   *          <code>program</code>
+   * @param program the JsProgram
+   */
+  public static void exec(JProgram jprogram, JsProgram program) {
+    StringVisitor v = new StringVisitor(jprogram, program.getScope());
+    v.accept(program);
+
+    Map<Integer, SortedSet<JsStringLiteral>> bins = new HashMap<Integer, SortedSet<JsStringLiteral>>();
+    for (int i = 0, j = program.getFragmentCount(); i < j; i++) {
+      bins.put(i, new TreeSet<JsStringLiteral>(LITERAL_COMPARATOR));
+    }
+    for (Map.Entry<JsStringLiteral, Integer> entry : v.fragmentAssignment.entrySet()) {
+      SortedSet<JsStringLiteral> set = bins.get(entry.getValue());
+      assert set != null;
+      set.add(entry.getKey());
+    }
+
+    for (Map.Entry<Integer, SortedSet<JsStringLiteral>> entry : bins.entrySet()) {
+      createVars(program, program.getFragmentBlock(entry.getKey()),
+          entry.getValue(), v.toCreate);
+    }
+  }
+
+  /**
    * Intern String literals that occur within a JsBlock. The symbol declarations
    * will be added as the first statement in the block.
    * 
@@ -160,50 +238,29 @@
    * @return <code>true</code> if any changes were made to the block
    */
   public static boolean exec(JsProgram program, JsBlock block, JsScope scope) {
-    StringVisitor v = new StringVisitor(scope);
+    StringVisitor v = new StringVisitor(null, scope);
     v.accept(block);
 
-    createVars(program, block, v.toCreate);
+    createVars(program, block, v.toCreate.keySet(), v.toCreate);
 
     return v.didChange();
   }
 
   /**
-   * Apply interning of String literals to a JsProgram. The symbol names for the
-   * interned strings will be defined within the program's top scope and the
-   * symbol declarations will be added as the first statement in the program's
-   * global block.
-   * 
-   * @param program the JsProgram
-   * @return a map from each created JsName to the string it is a literal for.
+   * Create variable declarations in <code>block</code> for literal strings
+   * <code>toCreate</code> using the variable map <code>names</code>.
    */
-  public static Map<JsName, String> exec(JsProgram program) {
-    StringVisitor v = new StringVisitor(program.getScope());
-    for (int i = 0; i < program.getFragmentCount(); i++) {
-      v.accept(program.getFragmentBlock(i));
-    }
-
-    Map<JsName, String> map = new HashMap<JsName, String>();
-    for (JsStringLiteral stringLit : v.toCreate.keySet()) {
-      map.put(v.toCreate.get(stringLit), stringLit.getValue());
-    }
-
-    createVars(program, program.getGlobalBlock(), v.toCreate);
-
-    return map;
-  }
-
   private static void createVars(JsProgram program, JsBlock block,
-      SortedMap<JsStringLiteral, JsName> toCreate) {
+      Collection<JsStringLiteral> toCreate, Map<JsStringLiteral, JsName> names) {
     if (toCreate.size() > 0) {
       // Create the pool of variable names.
       JsVars vars = new JsVars(program.createSourceInfoSynthetic(
           JsStringInterner.class, "Interned string pool"));
       SourceInfo sourceInfo = program.createSourceInfoSynthetic(
           JsStringInterner.class, "Interned string assignment");
-      for (Map.Entry<JsStringLiteral, JsName> entry : toCreate.entrySet()) {
-        JsVar var = new JsVar(sourceInfo, entry.getValue());
-        var.setInitExpr(entry.getKey());
+      for (JsStringLiteral literal : toCreate) {
+        JsVar var = new JsVar(sourceInfo, names.get(literal));
+        var.setInitExpr(literal);
         vars.add(var);
       }
       block.getStatements().add(0, vars);
diff --git a/dev/core/src/com/google/gwt/dev/js/ast/JsProgram.java b/dev/core/src/com/google/gwt/dev/js/ast/JsProgram.java
index dc073f9..bc50847 100644
--- a/dev/core/src/com/google/gwt/dev/js/ast/JsProgram.java
+++ b/dev/core/src/com/google/gwt/dev/js/ast/JsProgram.java
@@ -209,9 +209,7 @@
   }
 
   /**
-   * Note: This is currently assumed not to be called after
-   * GenerateJavaScriptAST has finished. If it ever is, then GWT.runAsync needs
-   * to be updated to account for such string literals.
+   * Creates or retrieves a JsStringLiteral from an interned object pool.
    */
   public JsStringLiteral getStringLiteral(SourceInfo sourceInfo, String value) {
     JsStringLiteral lit = stringLiteralMap.get(value);
diff --git a/dev/core/test/com/google/gwt/dev/javac/JProgramLastFragmentLoadingBeforeTest.java b/dev/core/test/com/google/gwt/dev/javac/JProgramLastFragmentLoadingBeforeTest.java
new file mode 100644
index 0000000..585f38e
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/javac/JProgramLastFragmentLoadingBeforeTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.javac;
+
+import com.google.gwt.dev.jjs.ast.JProgram;
+import com.google.gwt.dev.util.collect.Lists;
+
+import junit.framework.TestCase;
+
+import java.util.List;
+
+/**
+ * Tests {@link JProgram#lastFragmentLoadingBefore(int, int...)}.
+ */
+public class JProgramLastFragmentLoadingBeforeTest extends TestCase {
+
+  public void testBasics() {
+    List<Integer> initialSeq = Lists.create(4, 3, 2);
+    int numSps = 10;
+
+    // Very simple
+    assertEquals(0, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 0,
+        1, 2, 3));
+
+    // Equal fragments load at the same time
+    assertEquals(0,
+        JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 0, 0));
+    assertEquals(1,
+        JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 1, 1));
+    assertEquals(3,
+        JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 3, 3));
+    assertEquals(6,
+        JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 6, 6));
+    assertEquals(11, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 11,
+        11));
+
+    // Zero loads first
+    assertEquals(0, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 11,
+        0));
+    assertEquals(0, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 10,
+        0));
+    assertEquals(0,
+        JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 3, 0));
+
+    // Initial sequence fragments load before all others
+    assertEquals(3, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 3,
+        10));
+    assertEquals(3, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 10,
+        3));
+
+    // Earlier initial sequence fragments load before the others
+    assertEquals(4, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 2,
+        3, 4));
+    assertEquals(3,
+        JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 2, 3));
+    assertEquals(4, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 4,
+        3, 2));
+
+    // For non-equal exclusive fragments, leftovers is the common predecessor
+    assertEquals(11, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 1,
+        7));
+
+    // Leftovers is before any exclusive
+    assertEquals(11, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 7,
+        11));
+  }
+
+  public void testWithEmptyInitial() {
+    List<Integer> initialSeq = Lists.create();
+    int numSps = 10;
+
+    // Simple case
+    assertEquals(0, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 0,
+        1, 2, 3));
+
+    // Equal fragments load at the same time
+    assertEquals(0,
+        JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 0, 0));
+    assertEquals(6,
+        JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 6, 6));
+    assertEquals(11, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 11,
+        11));
+
+    // Zero loads first
+    assertEquals(0, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 11,
+        0));
+    assertEquals(0, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 10,
+        0));
+    assertEquals(0,
+        JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 3, 0));
+
+    // For non-equal exclusive fragments, leftovers is the common predecessor
+    assertEquals(11, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 1,
+        7));
+
+    // Leftovers is before any exclusive
+    assertEquals(11, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 7,
+        11));
+
+    // With just one argument, return it
+    assertEquals(0, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 0));
+    assertEquals(3, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 3));
+    assertEquals(11, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 11));
+  }
+
+  public void testWithNoSplitPoints() {
+    List<Integer> initialSeq = Lists.create();
+    int numSps = 0;
+
+    assertEquals(0,
+        JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 0, 0));
+    assertEquals(0, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 0,
+        0, 0));
+    assertEquals(0, JProgram.lastFragmentLoadingBefore(initialSeq, numSps, 0));
+  }
+}
diff --git a/dev/core/test/com/google/gwt/dev/javac/JavaCompilationSuite.java b/dev/core/test/com/google/gwt/dev/javac/JavaCompilationSuite.java
index c6efa75..9b0ceaa 100644
--- a/dev/core/test/com/google/gwt/dev/javac/JavaCompilationSuite.java
+++ b/dev/core/test/com/google/gwt/dev/javac/JavaCompilationSuite.java
@@ -33,6 +33,7 @@
     suite.addTestSuite(GWTProblemTest.class);
     suite.addTestSuite(JdtBehaviorTest.class);
     suite.addTestSuite(JdtCompilerTest.class);
+    suite.addTestSuite(JProgramLastFragmentLoadingBeforeTest.class);
     suite.addTestSuite(JSORestrictionsTest.class);
     suite.addTestSuite(JsniCheckerTest.class);
     suite.addTestSuite(TypeOracleMediatorTest.class);