Add soft permutations to the GWT compiler.
http://gwt-code-reviews.appspot.com/160801/show
Patch by: bobv
Review by: scottb, spoon


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7790 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/CompilationResult.java b/dev/core/src/com/google/gwt/core/ext/linker/CompilationResult.java
index cd0d96a..a83a59a 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/CompilationResult.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/CompilationResult.java
@@ -57,6 +57,12 @@
   public abstract SortedSet<SortedMap<SelectionProperty, String>> getPropertyMap();
 
   /**
+   * Returns the permutations of the collapsed deferred-binding property values
+   * that are compiled into the CompilationResult.
+   */
+  public abstract SoftPermutation[] getSoftPermutations();
+
+  /**
    * Returns the statement ranges for the JavaScript returned by
    * {@link #getJavaScript()}. Some subclasses return <code>null</code>, in
    * which case there is no statement range information available.
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/SoftPermutation.java b/dev/core/src/com/google/gwt/core/ext/linker/SoftPermutation.java
new file mode 100644
index 0000000..08c9410
--- /dev/null
+++ b/dev/core/src/com/google/gwt/core/ext/linker/SoftPermutation.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010 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.ext.linker;
+
+import java.io.Serializable;
+import java.util.SortedMap;
+
+/**
+ * Represents a permutation of collapsed deferred-binding property values.
+ */
+public abstract class SoftPermutation implements Serializable {
+
+  /**
+   * Returns the soft permutation id that should be passed into
+   * <code>gwtOnLoad</code>. The range of ids used for a compilation's soft
+   * permutations may be disjoint and may not correspond to the index of the
+   * SoftPermutation within the array returned from
+   * {@link CompilationResult#getSoftPermutations()}.
+   */
+  public abstract int getId();
+
+  /**
+   * Returns only the collapsed selection properties that resulted in the
+   * particular soft permutation. The SelectionProperties used may be disjoint
+   * from the properties returned by {@link CompilationResult#getPropertyMap()}.
+   */
+  public abstract SortedMap<SelectionProperty, String> getPropertyMap();
+}
\ No newline at end of file
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionInformation.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionInformation.java
index f0980a6..f00eb51 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionInformation.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionInformation.java
@@ -32,19 +32,27 @@
 public class SelectionInformation extends Artifact<SelectionInformation> {
   private final int hashCode;
   private final TreeMap<String, String> propMap;
+  private final int softPermutationId;
   private final String strongName;
 
-  public SelectionInformation(String strongName, TreeMap<String, String> propMap) {
+  public SelectionInformation(String strongName, int softPermutationId,
+      TreeMap<String, String> propMap) {
     super(SelectionScriptLinker.class);
     this.strongName = strongName;
+    this.softPermutationId = softPermutationId;
     this.propMap = propMap;
-    hashCode = strongName.hashCode() + propMap.hashCode() * 17 + 11;
+    hashCode = strongName.hashCode() + softPermutationId * 19
+        + propMap.hashCode() * 17 + 11;
   }
 
   public TreeMap<String, String> getPropMap() {
     return propMap;
   }
 
+  public int getSoftPermutationId() {
+    return softPermutationId;
+  }
+
   public String getStrongName() {
     return strongName;
   }
@@ -62,6 +70,11 @@
       return cmp;
     }
 
+    cmp = getSoftPermutationId() - o.getSoftPermutationId();
+    if (cmp != 0) {
+      return cmp;
+    }
+
     // compare the size of the property maps
     if (getPropMap().size() != o.getPropMap().size()) {
       return getPropMap().size() - o.getPropMap().size();
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
index d78ee8d..7a805d0 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
@@ -25,7 +25,9 @@
 import com.google.gwt.core.ext.linker.EmittedArtifact;
 import com.google.gwt.core.ext.linker.ScriptReference;
 import com.google.gwt.core.ext.linker.SelectionProperty;
+import com.google.gwt.core.ext.linker.SoftPermutation;
 import com.google.gwt.core.ext.linker.StylesheetReference;
+import com.google.gwt.dev.util.StringKey;
 import com.google.gwt.dev.util.Util;
 import com.google.gwt.dev.util.collect.HashSet;
 import com.google.gwt.dev.util.collect.Lists;
@@ -55,6 +57,30 @@
    */
 
   /**
+   * This represents the combination of a unique content hash (i.e. the MD5 of
+   * the bytes to be written into the cache.html file) and a soft permutation
+   * id.
+   */
+  protected static class PermutationId extends StringKey {
+    private final int softPermutationId;
+    private final String strongName;
+
+    public PermutationId(String strongName, int softPermutationId) {
+      super(strongName + ":" + softPermutationId);
+      this.strongName = strongName;
+      this.softPermutationId = softPermutationId;
+    }
+
+    public int getSoftPermutationId() {
+      return softPermutationId;
+    }
+
+    public String getStrongName() {
+      return strongName;
+    }
+  }
+
+  /**
    * The extension added to demand-loaded fragment files.
    */
   protected static final String FRAGMENT_EXTENSION = ".cache.js";
@@ -111,11 +137,11 @@
   }
 
   /**
-   * This maps each compilation strong name to the property settings for that
+   * This maps each unique permutation to the property settings for that
    * compilation. A single compilation can have multiple property settings if
    * the compiles for those settings yielded the exact same compiled output.
    */
-  private final SortedMap<String, List<Map<String, String>>> propMapsByStrongName = new TreeMap<String, List<Map<String, String>>>();
+  private final SortedMap<PermutationId, List<Map<String, String>>> propMapsByPermutation = new TreeMap<PermutationId, List<Map<String, String>>>();
 
   /**
    * This method is left in place for existing subclasses of
@@ -321,21 +347,21 @@
     startPos = selectionScript.indexOf("// __PERMUTATIONS_END__");
     if (startPos != -1) {
       StringBuffer text = new StringBuffer();
-      if (propMapsByStrongName.size() == 0) {
+      if (propMapsByPermutation.size() == 0) {
         // Hosted mode link.
         text.append("alert(\"GWT module '" + context.getModuleName()
             + "' may need to be (re)compiled\");");
         text.append("return;");
 
-      } else if (propMapsByStrongName.size() == 1) {
+      } else if (propMapsByPermutation.size() == 1) {
         // Just one distinct compilation; no need to evaluate properties
         text.append("strongName = '"
-            + propMapsByStrongName.keySet().iterator().next() + "';");
+            + propMapsByPermutation.keySet().iterator().next() + "';");
       } else {
         Set<String> propertiesUsed = new HashSet<String>();
-        for (String strongName : propMapsByStrongName.keySet()) {
-          for (Map<String, String> propertyMap : propMapsByStrongName.get(strongName)) {
-            // unflatten([v1, v2, v3], 'strongName');
+        for (PermutationId permutationId : propMapsByPermutation.keySet()) {
+          for (Map<String, String> propertyMap : propMapsByPermutation.get(permutationId)) {
+            // unflatten([v1, v2, v3], 'strongName' + ':softPermId');
             text.append("unflattenKeylistIntoAnswers([");
             boolean needsComma = false;
             for (SelectionProperty p : context.getProperties()) {
@@ -353,7 +379,11 @@
               text.append("'" + propertyMap.get(p.getName()) + "'");
               propertiesUsed.add(p.getName());
             }
-            text.append("], '").append(strongName).append("');\n");
+
+            // Concatenate the soft permutation id to improve string interning
+            text.append("], '").append(permutationId.getStrongName()).append(
+                "' + ':").append(permutationId.getSoftPermutationId()).append(
+                "');\n");
           }
         }
 
@@ -457,7 +487,17 @@
       for (Map.Entry<SelectionProperty, String> entry : propertyMap.entrySet()) {
         propMap.put(entry.getKey().getName(), entry.getValue());
       }
-      emitted.add(new SelectionInformation(strongName, propMap));
+
+      // The soft properties may not be a subset of the existing set
+      for (SoftPermutation soft : result.getSoftPermutations()) {
+        // Make a copy we can add add more properties to
+        TreeMap<String, String> softMap = new TreeMap<String, String>(propMap);
+        // Make sure this SelectionInformation contains the soft properties
+        for (Map.Entry<SelectionProperty, String> entry : soft.getPropertyMap().entrySet()) {
+          softMap.put(entry.getKey().getName(), entry.getValue());
+        }
+        emitted.add(new SelectionInformation(strongName, soft.getId(), softMap));
+      }
     }
 
     return emitted;
@@ -466,13 +506,14 @@
   private Map<String, String> processSelectionInformation(
       SelectionInformation selInfo) {
     TreeMap<String, String> entries = selInfo.getPropMap();
-    String strongName = selInfo.getStrongName();
-    if (!propMapsByStrongName.containsKey(strongName)) {
-      propMapsByStrongName.put(strongName,
+    PermutationId permutationId = new PermutationId(selInfo.getStrongName(),
+        selInfo.getSoftPermutationId());
+    if (!propMapsByPermutation.containsKey(permutationId)) {
+      propMapsByPermutation.put(permutationId,
           Lists.<Map<String, String>> create(entries));
     } else {
-      propMapsByStrongName.put(strongName, Lists.add(
-          propMapsByStrongName.get(strongName), entries));
+      propMapsByPermutation.put(permutationId, Lists.add(
+          propMapsByPermutation.get(permutationId), entries));
     }
     return entries;
   }
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardCompilationResult.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardCompilationResult.java
index c8776a0..b3fa907 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardCompilationResult.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardCompilationResult.java
@@ -17,16 +17,19 @@
 
 import com.google.gwt.core.ext.linker.CompilationResult;
 import com.google.gwt.core.ext.linker.SelectionProperty;
+import com.google.gwt.core.ext.linker.SoftPermutation;
 import com.google.gwt.core.ext.linker.StatementRanges;
 import com.google.gwt.core.ext.linker.SymbolData;
 import com.google.gwt.dev.jjs.PermutationResult;
 import com.google.gwt.dev.util.DiskCache;
 import com.google.gwt.dev.util.Util;
+import com.google.gwt.dev.util.collect.Lists;
 
 import java.io.Serializable;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.SortedMap;
 import java.util.SortedSet;
@@ -64,6 +67,8 @@
     }
   }
 
+  private static final SoftPermutation[] EMPTY_SOFT_PERMUTATION_ARRAY = {};
+
   /**
    * Smaller maps come before larger maps, then we compare the concatenation of
    * every value.
@@ -77,6 +82,8 @@
   private final SortedSet<SortedMap<SelectionProperty, String>> propertyValues = new TreeSet<SortedMap<SelectionProperty, String>>(
       MAP_COMPARATOR);
 
+  private List<SoftPermutation> softPermutations = Lists.create();
+
   private final StatementRanges[] statementRanges;
 
   private final String strongName;
@@ -110,6 +117,11 @@
     propertyValues.add(Collections.unmodifiableSortedMap(map));
   }
 
+  public void addSoftPermutation(Map<SelectionProperty, String> propertyMap) {
+    softPermutations = Lists.add(softPermutations, new StandardSoftPermutation(
+        softPermutations.size(), propertyMap));
+  }
+
   @Override
   public String[] getJavaScript() {
     String[] js = new String[jsToken.length];
@@ -130,6 +142,11 @@
   }
 
   @Override
+  public SoftPermutation[] getSoftPermutations() {
+    return softPermutations.toArray(new SoftPermutation[softPermutations.size()]);
+  }
+
+  @Override
   public StatementRanges[] getStatementRanges() {
     return statementRanges;
   }
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardSoftPermutation.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardSoftPermutation.java
new file mode 100644
index 0000000..3a61569
--- /dev/null
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardSoftPermutation.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2010 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.ext.linker.impl;
+
+import com.google.gwt.core.ext.linker.SelectionProperty;
+import com.google.gwt.core.ext.linker.SoftPermutation;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * The standard implementation of {@link SoftPermutation}.
+ */
+public class StandardSoftPermutation extends SoftPermutation {
+  private final int id;
+  private final SortedMap<SelectionProperty, String> propertyMap = new TreeMap<SelectionProperty, String>(
+      StandardLinkerContext.SELECTION_PROPERTY_COMPARATOR);
+
+  public StandardSoftPermutation(int id,
+      Map<SelectionProperty, String> propertyMap) {
+    this.id = id;
+    this.propertyMap.putAll(propertyMap);
+  }
+
+  @Override
+  public int getId() {
+    return id;
+  }
+
+  @Override
+  public SortedMap<SelectionProperty, String> getPropertyMap() {
+    return Collections.unmodifiableSortedMap(propertyMap);
+  }
+
+  /**
+   * For debugging use only.
+   */
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append("ID ").append(getId()).append(" = {");
+    for (Map.Entry<SelectionProperty, String> entry : propertyMap.entrySet()) {
+      sb.append(" ").append(entry.getKey().getName()).append(":").append(
+          entry.getValue());
+    }
+    sb.append(" }");
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/dev/core/src/com/google/gwt/core/linker/IFrameTemplate.js b/dev/core/src/com/google/gwt/core/linker/IFrameTemplate.js
index d4cb5e1..4da339a 100644
--- a/dev/core/src/com/google/gwt/core/linker/IFrameTemplate.js
+++ b/dev/core/src/com/google/gwt/core/linker/IFrameTemplate.js
@@ -42,6 +42,9 @@
   // the strong name of the cache.js file to load.
   ,answers = []
 
+  // Provides the module with the soft permutation id
+  ,softPermutationId = 0
+
   // Error functions.  Default unset in compiled mode, may be set by meta props.
   ,onLoadErrorFunc, propertyErrorFunc
 
@@ -98,7 +101,7 @@
       // remove this whole function from the global namespace to allow GC
       __MODULE_FUNC__ = null;
       // JavaToJavaScriptCompiler logs onModuleLoadStart for each EntryPoint.
-      frameWnd.gwtOnLoad(onLoadErrorFunc, '__MODULE_NAME__', base);
+      frameWnd.gwtOnLoad(onLoadErrorFunc, '__MODULE_NAME__', base, softPermutationId);
       // Record when the module EntryPoints return.
       $stats && $stats({
         moduleName: '__MODULE_NAME__',
@@ -267,6 +270,11 @@
 // __PERMUTATIONS_BEGIN__
       // Permutation logic
 // __PERMUTATIONS_END__
+      var idx = strongName.indexOf(':');
+      if (idx != -1) {
+        softPermutationId = Number(strongName.substring(idx + 1));
+        strongName = strongName.substring(0, idx);
+      }
       initialHtml = strongName + ".cache.html";
     } catch (e) {
       // intentionally silent on property failure
diff --git a/dev/core/src/com/google/gwt/core/linker/XSTemplate.js b/dev/core/src/com/google/gwt/core/linker/XSTemplate.js
index f432d27..5200a69 100644
--- a/dev/core/src/com/google/gwt/core/linker/XSTemplate.js
+++ b/dev/core/src/com/google/gwt/core/linker/XSTemplate.js
@@ -41,6 +41,9 @@
   // the strong name of the cache.js file to load.
   ,answers = []
 
+  // Provides the module with the soft permutation id
+  ,softPermutationId = 0
+
   // Error functions.  Default unset in compiled mode, may be set by meta props.
   ,onLoadErrorFunc, propertyErrorFunc
 
@@ -82,7 +85,7 @@
   function maybeStartModule() {
     // TODO: it may not be necessary to check gwtOnLoad here.
     if (gwtOnLoad && bodyDone) {
-      gwtOnLoad(onLoadErrorFunc, '__MODULE_NAME__', base);
+      gwtOnLoad(onLoadErrorFunc, '__MODULE_NAME__', base, softPermutationId);
       // Record when the module EntryPoints return.
       $stats && $stats({
         moduleName: '__MODULE_NAME__',
@@ -194,6 +197,11 @@
 // __PERMUTATIONS_BEGIN__
     // Permutation logic
 // __PERMUTATIONS_END__
+    var idx = strongName.indexOf(':');
+    if (idx != -1) {
+      softPermutationId = Number(strongName.substring(idx + 1));
+      strongName = strongName.substring(0, idx);
+    }
   } catch (e) {
     // intentionally silent on property failure
     return;
diff --git a/dev/core/src/com/google/gwt/dev/CompilePerms.java b/dev/core/src/com/google/gwt/dev/CompilePerms.java
index 0a0a5b6..8b7eb50 100644
--- a/dev/core/src/com/google/gwt/dev/CompilePerms.java
+++ b/dev/core/src/com/google/gwt/dev/CompilePerms.java
@@ -334,9 +334,10 @@
     ModuleDef module = ModuleDefLoader.loadFromClassPath(logger, moduleName);
     PropertyPermutations allPermutations = new PropertyPermutations(
         module.getProperties(), module.getActiveLinkerNames());
+    List<PropertyPermutations> collapsedPermutations = allPermutations.collapseProperties();
     int[] perms = options.getPermsToCompile();
     if (perms == null) {
-      perms = new int[allPermutations.size()];
+      perms = new int[collapsedPermutations.size()];
       for (int i = 0; i < perms.length; ++i) {
         perms[i] = i;
       }
@@ -347,12 +348,10 @@
     for (int permId : perms) {
       /*
        * TODO(spoon,scottb): move Precompile out of the loop to run only once
-       * per shard. We'll need a new PropertyPermutations constructor that can
-       * take a precise list. Then figure out a way to avoid copying the
-       * generated artifacts into every perm result on a shard.
+       * per shard. Then figure out a way to avoid copying the generated
+       * artifacts into every perm result on a shard.
        */
-      PropertyPermutations onePerm = new PropertyPermutations(allPermutations,
-          permId, 1);
+      PropertyPermutations onePerm = collapsedPermutations.get(permId);
 
       assert (precompilationOptions.getDumpSignatureFile() == null);
       Precompilation precompilation = Precompile.precompile(logger,
diff --git a/dev/core/src/com/google/gwt/dev/Link.java b/dev/core/src/com/google/gwt/dev/Link.java
index b585ccc..cbaf113 100644
--- a/dev/core/src/com/google/gwt/dev/Link.java
+++ b/dev/core/src/com/google/gwt/dev/Link.java
@@ -301,6 +301,8 @@
     for (StaticPropertyOracle propOracle : perm.getPropertyOracles()) {
       compilation.addSelectionPermutation(computeSelectionPermutation(
           linkerContext, propOracle));
+      compilation.addSoftPermutation(computeSoftPermutation(linkerContext,
+          propOracle));
     }
   }
 
@@ -352,6 +354,22 @@
     return unboundProperties;
   }
 
+  private static Map<SelectionProperty, String> computeSoftPermutation(
+      StandardLinkerContext linkerContext, StaticPropertyOracle propOracle) {
+    BindingProperty[] orderedProps = propOracle.getOrderedProps();
+    String[] orderedPropValues = propOracle.getOrderedPropValues();
+    Map<SelectionProperty, String> softProperties = new HashMap<SelectionProperty, String>();
+    for (int i = 0; i < orderedProps.length; i++) {
+      if (orderedProps[i].getCollapsedValues().isEmpty()) {
+        continue;
+      }
+
+      SelectionProperty key = linkerContext.getProperty(orderedProps[i].getName());
+      softProperties.put(key, orderedPropValues[i]);
+    }
+    return softProperties;
+  }
+
   /**
    * Emit final output.
    */
@@ -584,7 +602,7 @@
       ModuleDef module, JJSOptions precompileOptions)
       throws UnableToCompleteException {
     int numPermutations = new PropertyPermutations(module.getProperties(),
-        module.getActiveLinkerNames()).size();
+        module.getActiveLinkerNames()).collapseProperties().size();
     List<File> resultFiles = new ArrayList<File>(numPermutations);
     for (int i = 0; i < numPermutations; ++i) {
       File f = CompilePerms.makePermFilename(compilerWorkDir, i);
diff --git a/dev/core/src/com/google/gwt/dev/Permutation.java b/dev/core/src/com/google/gwt/dev/Permutation.java
index c0319b1..8c9afef 100644
--- a/dev/core/src/com/google/gwt/dev/Permutation.java
+++ b/dev/core/src/com/google/gwt/dev/Permutation.java
@@ -16,10 +16,9 @@
 package com.google.gwt.dev;
 
 import com.google.gwt.dev.cfg.StaticPropertyOracle;
+import com.google.gwt.dev.util.collect.Lists;
 
 import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.SortedMap;
 import java.util.SortedSet;
@@ -29,54 +28,93 @@
  * Represents the state of a single permutation for compile.
  */
 public final class Permutation implements Serializable {
+
   private final int id;
-  private final List<StaticPropertyOracle> propertyOracles = new ArrayList<StaticPropertyOracle>();
-  private final SortedMap<String, String> rebindAnswers = new TreeMap<String, String>();
+
+  private List<StaticPropertyOracle> orderedPropertyOracles = Lists.create();
+  private List<SortedMap<String, String>> orderedRebindAnswers = Lists.create();
 
   /**
    * Clones an existing permutation, but with a new id.
-   *  
+   * 
    * @param id new permutation id
    * @param other Permutation to copy
    */
   public Permutation(int id, Permutation other) {
     this.id = id;
-    this.propertyOracles.addAll(other.propertyOracles);
-    this.rebindAnswers.putAll(other.rebindAnswers);
+    orderedPropertyOracles = Lists.create(other.orderedPropertyOracles);
+    orderedRebindAnswers = Lists.create(other.orderedRebindAnswers);
   }
 
   public Permutation(int id, StaticPropertyOracle propertyOracle) {
     this.id = id;
-    this.propertyOracles.add(propertyOracle);
+    orderedPropertyOracles = Lists.add(orderedPropertyOracles, propertyOracle);
+    orderedRebindAnswers = Lists.add(orderedRebindAnswers,
+        new TreeMap<String, String>());
   }
 
   public int getId() {
     return id;
   }
 
+  public SortedMap<String, String>[] getOrderedRebindAnswers() {
+    @SuppressWarnings("unchecked")
+    SortedMap<String, String>[] arr = new SortedMap[orderedRebindAnswers.size()];
+    return orderedRebindAnswers.toArray(arr);
+  }
+
+  /**
+   * Returns the property oracles, sorted by property values.
+   */
   public StaticPropertyOracle[] getPropertyOracles() {
-    return propertyOracles.toArray(new StaticPropertyOracle[propertyOracles.size()]);
+    return orderedPropertyOracles.toArray(new StaticPropertyOracle[orderedPropertyOracles.size()]);
   }
 
-  public SortedMap<String, String> getRebindAnswers() {
-    return Collections.unmodifiableSortedMap(rebindAnswers);
-  }
-
+  /**
+   * This is called to merge two permutations that either have identical rebind
+   * answers or were explicitly collapsed using <collapse-property>
+   */
   public void mergeFrom(Permutation other, SortedSet<String> liveRebindRequests) {
     if (getClass().desiredAssertionStatus()) {
-      for (String rebindRequest : liveRebindRequests) {
-        String myAnswer = rebindAnswers.get(rebindRequest);
-        String otherAnswer = other.rebindAnswers.get(rebindRequest);
-        assert myAnswer.equals(otherAnswer);
+      for (SortedMap<String, String> myRebind : orderedRebindAnswers) {
+        for (SortedMap<String, String> otherRebind : other.orderedRebindAnswers) {
+          for (String rebindRequest : liveRebindRequests) {
+            String myAnswer = myRebind.get(rebindRequest);
+            String otherAnswer = otherRebind.get(rebindRequest);
+            assert myAnswer.equals(otherAnswer);
+          }
+        }
       }
     }
-    assert !propertyOracles.isEmpty();
-    assert !other.propertyOracles.isEmpty();
-    propertyOracles.addAll(other.propertyOracles);
-    other.propertyOracles.clear();
+    mergeRebindsFromCollapsed(other);
+  }
+
+  /**
+   * This is called to collapse one permutation into another where the rebinds
+   * vary between the two permutations.
+   */
+  public void mergeRebindsFromCollapsed(Permutation other) {
+    assert other.orderedPropertyOracles.size() == other.orderedRebindAnswers.size();
+    orderedPropertyOracles = Lists.addAll(orderedPropertyOracles,
+        other.orderedPropertyOracles);
+    orderedRebindAnswers = Lists.addAll(orderedRebindAnswers,
+        other.orderedRebindAnswers);
+    other.destroy();
   }
 
   public void putRebindAnswer(String requestType, String resultType) {
-    rebindAnswers.put(requestType, resultType);
+    assert orderedRebindAnswers.size() == 1 : "Cannot add rebind to merged Permutation";
+    SortedMap<String, String> answerMap = orderedRebindAnswers.get(0);
+    assert answerMap != null;
+    answerMap.put(requestType, resultType);
+  }
+
+  /**
+   * Clear the state of the Permutation. This aids the correctness-checking code
+   * in {@link #mergeFrom}.
+   */
+  private void destroy() {
+    orderedPropertyOracles = Lists.create();
+    orderedRebindAnswers = Lists.create();
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/Precompile.java b/dev/core/src/com/google/gwt/dev/Precompile.java
index 7a41eff..d2aac7b 100644
--- a/dev/core/src/com/google/gwt/dev/Precompile.java
+++ b/dev/core/src/com/google/gwt/dev/Precompile.java
@@ -42,6 +42,7 @@
 import com.google.gwt.dev.shell.CheckForUpdates;
 import com.google.gwt.dev.shell.StandardRebindOracle;
 import com.google.gwt.dev.shell.CheckForUpdates.UpdateResult;
+import com.google.gwt.dev.util.CollapsedPropertyKey;
 import com.google.gwt.dev.util.Memory;
 import com.google.gwt.dev.util.PerfLogger;
 import com.google.gwt.dev.util.Util;
@@ -68,11 +69,17 @@
 import com.google.gwt.dev.util.arg.OptionGenDir;
 import com.google.gwt.dev.util.arg.OptionMaxPermsPerPrecompile;
 import com.google.gwt.dev.util.arg.OptionValidateOnly;
+import com.google.gwt.dev.util.collect.Lists;
 
 import java.io.File;
 import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.SortedMap;
 import java.util.SortedSet;
@@ -517,16 +524,18 @@
       PerfLogger.end();
 
       // Merge all identical permutations together.
-      Permutation[] permutations = rpo.getPermutations();
+      List<Permutation> permutations = new ArrayList<Permutation>(
+          Arrays.asList(rpo.getPermutations()));
+
+      mergeCollapsedPermutations(permutations);
+
       // Sort the permutations by an ordered key to ensure determinism.
-      SortedMap<String, Permutation> merged = new TreeMap<String, Permutation>();
+      SortedMap<RebindAnswersPermutationKey, Permutation> merged = new TreeMap<RebindAnswersPermutationKey, Permutation>();
       SortedSet<String> liveRebindRequests = unifiedAst.getRebindRequests();
       for (Permutation permutation : permutations) {
-        // Construct a key from the stringified map of live rebind answers.
-        SortedMap<String, String> rebindAnswers = new TreeMap<String, String>(
-            permutation.getRebindAnswers());
-        rebindAnswers.keySet().retainAll(liveRebindRequests);
-        String key = rebindAnswers.toString();
+        // Construct a key for the live rebind answers.
+        RebindAnswersPermutationKey key = new RebindAnswersPermutationKey(
+            permutation, liveRebindRequests);
         if (merged.containsKey(key)) {
           Permutation existing = merged.get(key);
           existing.mergeFrom(permutation, liveRebindRequests);
@@ -534,6 +543,7 @@
           merged.put(key, permutation);
         }
       }
+
       return new Precompilation(unifiedAst, merged.values(), permutationBase,
           generatedArtifacts);
     } catch (UnableToCompleteException e) {
@@ -565,6 +575,54 @@
         + compilerClassName + "'", caught);
   }
 
+  /**
+   * This merges Permutations that can be considered equivalent by considering
+   * their collapsed properties. The list passed into this method may have
+   * elements removed from it.
+   */
+  private static void mergeCollapsedPermutations(List<Permutation> permutations) {
+    if (permutations.size() < 2) {
+      return;
+    }
+
+    // See the doc for CollapsedPropertyKey
+    SortedMap<CollapsedPropertyKey, List<Permutation>> mergedByCollapsedProperties = new TreeMap<CollapsedPropertyKey, List<Permutation>>();
+
+    // This loop creates the equivalence sets
+    for (Iterator<Permutation> it = permutations.iterator(); it.hasNext();) {
+      Permutation entry = it.next();
+      CollapsedPropertyKey key = new CollapsedPropertyKey(entry);
+
+      List<Permutation> equivalenceSet = mergedByCollapsedProperties.get(key);
+      if (equivalenceSet == null) {
+        equivalenceSet = Lists.create();
+      } else {
+        // Mutate list
+        it.remove();
+        equivalenceSet = Lists.add(equivalenceSet, entry);
+      }
+      mergedByCollapsedProperties.put(key, equivalenceSet);
+    }
+
+    // This loop merges the Permutations together
+    for (Map.Entry<CollapsedPropertyKey, List<Permutation>> entry : mergedByCollapsedProperties.entrySet()) {
+      Permutation mergeInto = entry.getKey().getPermutation();
+
+      /*
+       * Merge the deferred-binding properties once we no longer need the
+       * PropertyOracle data from the extra permutations.
+       */
+      for (Permutation mergeFrom : entry.getValue()) {
+        mergeInto.mergeRebindsFromCollapsed(mergeFrom);
+      }
+    }
+
+    // Renumber the Permutations
+    for (int i = 0, j = permutations.size(); i < j; i++) {
+      permutations.set(i, new Permutation(i, permutations.get(i)));
+    }
+  }
+
   private final PrecompileOptionsImpl options;
 
   public Precompile(PrecompileOptions options) {
@@ -625,7 +683,7 @@
             "Precompiling (minimal) module " + module.getName());
         Util.writeObjectAsFile(logger, precompilationFile, options);
         int numPermutations = new PropertyPermutations(module.getProperties(),
-            module.getActiveLinkerNames()).size();
+            module.getActiveLinkerNames()).collapseProperties().size();
         Util.writeStringAsFile(logger, new File(compilerWorkDir,
             PERM_COUNT_FILENAME), String.valueOf(numPermutations));
         branch.log(TreeLogger.INFO,
diff --git a/dev/core/src/com/google/gwt/dev/RebindAnswersPermutationKey.java b/dev/core/src/com/google/gwt/dev/RebindAnswersPermutationKey.java
new file mode 100644
index 0000000..29e8860
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/RebindAnswersPermutationKey.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2010 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;
+
+import com.google.gwt.dev.util.StringKey;
+
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * Creates a string representation of live rebound types to all possible
+ * answers.
+ * 
+ * <pre>
+ * {
+ *   Foo = [ FooImpl ], // A "hard" rebind
+ *   Bundle = [ Bundle_EN, Bundle_FR ]  // A "soft" rebind
+ * }
+ * </pre>
+ */
+class RebindAnswersPermutationKey extends StringKey {
+  private static String collapse(Permutation permutation,
+      SortedSet<String> liveRebindRequests) {
+    // Accumulates state
+    SortedMap<String, SortedSet<String>> answers = new TreeMap<String, SortedSet<String>>();
+
+    // Iterate over each map of rebind answers
+    for (SortedMap<String, String> rebinds : permutation.getOrderedRebindAnswers()) {
+      for (Map.Entry<String, String> rebind : rebinds.entrySet()) {
+        if (!liveRebindRequests.contains(rebind.getKey())) {
+          // Ignore rebinds that aren't actually used
+          continue;
+        }
+
+        // Get-or-put
+        SortedSet<String> set = answers.get(rebind.getKey());
+        if (set == null) {
+          set = new TreeSet<String>();
+          answers.put(rebind.getKey(), set);
+        }
+
+        // Record rebind value
+        set.add(rebind.getValue());
+      }
+    }
+
+    // Create string
+    return answers.toString();
+  }
+
+  public RebindAnswersPermutationKey(Permutation permutation,
+      SortedSet<String> liveRebindRequests) {
+    super(collapse(permutation, liveRebindRequests));
+  }
+}
\ No newline at end of file
diff --git a/dev/core/src/com/google/gwt/dev/cfg/BindingProperty.java b/dev/core/src/com/google/gwt/dev/cfg/BindingProperty.java
index 6eb07a5..5bea795 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/BindingProperty.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/BindingProperty.java
@@ -15,15 +15,24 @@
  */
 package com.google.gwt.dev.cfg;
 
+import com.google.gwt.dev.util.collect.IdentityHashSet;
+import com.google.gwt.dev.util.collect.Lists;
 import com.google.gwt.dev.util.collect.Sets;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
+import java.util.regex.Pattern;
 
 /**
  * Represents a single named deferred binding or configuration property that can
@@ -32,9 +41,10 @@
  * of the defined set.
  */
 public class BindingProperty extends Property {
-
+  public static final String GLOB_STAR = "*";
   private static final String EMPTY = "";
 
+  private List<SortedSet<String>> collapsedValues = Lists.create();
   private final Map<Condition, SortedSet<String>> conditionalValues = new LinkedHashMap<Condition, SortedSet<String>>();
   private final SortedSet<String> definedValues = new TreeSet<String>();
   private PropertyProvider provider;
@@ -50,6 +60,27 @@
     fallback = EMPTY;
   }
 
+  /**
+   * Add an equivalence set of property values.
+   */
+  public void addCollapsedValues(String... values) {
+
+    // Sanity check caller
+    for (String value : values) {
+      if (value.contains(GLOB_STAR)) {
+        // Expanded in normalizeCollapsedValues()
+        continue;
+      } else if (!definedValues.contains(value)) {
+        throw new IllegalArgumentException(
+            "Attempting to collapse unknown value " + value);
+      }
+    }
+
+    // We want a mutable set, because it simplifies normalizeCollapsedValues
+    SortedSet<String> temp = new TreeSet<String>(Arrays.asList(values));
+    collapsedValues = Lists.add(collapsedValues, temp);
+  }
+
   public void addDefinedValue(Condition condition, String newValue) {
     definedValues.add(newValue);
     SortedSet<String> set = conditionalValues.get(condition);
@@ -70,6 +101,10 @@
     return allowedValues.toArray(new String[allowedValues.size()]);
   }
 
+  public List<SortedSet<String>> getCollapsedValues() {
+    return collapsedValues;
+  }
+
   public Map<Condition, SortedSet<String>> getConditionalValues() {
     return Collections.unmodifiableMap(conditionalValues);
   }
@@ -191,4 +226,82 @@
   public void setProvider(PropertyProvider provider) {
     this.provider = provider;
   }
+
+  /**
+   * Create a minimal number of equivalence sets, expanding any glob patterns.
+   */
+  void normalizeCollapsedValues() {
+    if (collapsedValues.isEmpty()) {
+      return;
+    }
+
+    // Expand globs
+    for (Set<String> set : collapsedValues) {
+      // Compile a regex that matches all glob expressions that we see
+      StringBuilder pattern = new StringBuilder();
+      for (Iterator<String> it = set.iterator(); it.hasNext();) {
+        String value = it.next();
+        if (value.contains(GLOB_STAR)) {
+          it.remove();
+          if (pattern.length() > 0) {
+            pattern.append("|");
+          }
+
+          // a*b ==> (a.*b)
+          pattern.append("(");
+          // We know value is a Java ident, so no special escaping is needed
+          pattern.append(value.replace(GLOB_STAR, ".*"));
+          pattern.append(")");
+        }
+      }
+
+      if (pattern.length() == 0) {
+        continue;
+      }
+
+      Pattern p = Pattern.compile(pattern.toString());
+      for (String definedValue : definedValues) {
+        if (p.matcher(definedValue).matches()) {
+          set.add(definedValue);
+        }
+      }
+    }
+
+    // Minimize number of sets
+
+    // Maps a value to the set that contains that value
+    Map<String, SortedSet<String>> map = new HashMap<String, SortedSet<String>>();
+
+    // For each equivalence set we have
+    for (SortedSet<String> set : collapsedValues) {
+      // Examine each original value in the set
+      for (String value : new LinkedHashSet<String>(set)) {
+        // See if the value was previously assigned to another set
+        SortedSet<String> existing = map.get(value);
+        if (existing == null) {
+          map.put(value, set);
+        } else {
+          // If so, merge the existing set into this one and update pointers
+          set.addAll(existing);
+          for (String mergedValue : existing) {
+            map.put(mergedValue, set);
+          }
+        }
+      }
+    }
+
+    // The values of the maps will now contain the minimal number of sets
+    collapsedValues = new ArrayList<SortedSet<String>>(
+        new IdentityHashSet<SortedSet<String>>(map.values()));
+
+    // Sort the list
+    Lists.sort(collapsedValues, new Comparator<SortedSet<String>>() {
+      public int compare(SortedSet<String> o1, SortedSet<String> o2) {
+        String s1 = o1.toString();
+        String s2 = o2.toString();
+        assert !s1.equals(s2) : "Should not have seen equal sets";
+        return s1.compareTo(s2);
+      }
+    });
+  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java b/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
index 5db5c05..ab7c61f 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java
@@ -77,6 +77,8 @@
 
   private String activePrimaryLinker;
 
+  private boolean collapseAllProperties;
+
   private final DefaultFilters defaultFilters;
 
   private final List<String> entryPointTypeNames = new ArrayList<String>();
@@ -400,6 +402,13 @@
   }
 
   /**
+   * Mainly for testing and decreasing compile times.
+   */
+  public void setCollapseAllProperties(boolean collapse) {
+    collapseAllProperties = collapse;
+  }
+  
+  /**
    * Override the module's apparent name. Setting this value to
    * <code>null<code> will disable the name override.
    */
@@ -435,6 +444,13 @@
     for (Property current : getProperties()) {
       if (current instanceof BindingProperty) {
         BindingProperty prop = (BindingProperty) current;
+        
+        if (collapseAllProperties) {
+          prop.addCollapsedValues("*");
+        }
+
+        prop.normalizeCollapsedValues();
+
         /*
          * Create a default property provider for any properties with more than
          * one possible value and no existing provider.
diff --git a/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java b/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
index c6ab34f..bbfc2ab 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
@@ -35,10 +35,12 @@
 import java.io.IOException;
 import java.io.StringReader;
 import java.net.URL;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 // CHECKSTYLE_NAMING_OFF
 /**
@@ -52,6 +54,12 @@
 
     protected final String __clear_configuration_property_1_name = null;
 
+    protected final String __collapse_all_properties_1_value = "true";
+
+    protected final String __collapse_property_1_name = null;
+
+    protected final String __collapse_property_2_values = null;
+
     protected final String __define_configuration_property_1_name = null;
 
     protected final String __define_configuration_property_2_is_multi_valued = null;
@@ -176,6 +184,52 @@
       return null;
     }
 
+    protected Schema __collapse_all_properties_begin(boolean collapse) {
+      moduleDef.setCollapseAllProperties(collapse);
+      return null;
+    }
+
+    protected Schema __collapse_property_begin(PropertyName name,
+        PropertyValueGlob[] values) throws UnableToCompleteException {
+      Property prop = moduleDef.getProperties().find(name.token);
+      if (prop == null) {
+        logger.log(TreeLogger.ERROR, "No property named " + name.token
+            + " has been defined");
+        throw new UnableToCompleteException();
+      } else if (!(prop instanceof BindingProperty)) {
+        logger.log(TreeLogger.ERROR, "The property " + name.token
+            + " is not a deferred-binding property");
+        throw new UnableToCompleteException();
+      }
+
+      BindingProperty binding = (BindingProperty) prop;
+      List<String> allowed = Arrays.asList(binding.getDefinedValues());
+
+      String[] tokens = new String[values.length];
+      boolean error = false;
+      for (int i = 0, j = values.length; i < j; i++) {
+        tokens[i] = values[i].token;
+        if (values[i].isGlob()) {
+          // Expanded later in BindingProperty
+          continue;
+        } else if (!allowed.contains(tokens[i])) {
+          logger.log(TreeLogger.ERROR, "The value " + tokens[i]
+              + " was not previously defined for the property "
+              + binding.getName());
+          error = true;
+        }
+      }
+
+      if (error) {
+        throw new UnableToCompleteException();
+      }
+
+      binding.addCollapsedValues(tokens);
+
+      // No children
+      return null;
+    }
+
     protected Schema __define_configuration_property_begin(PropertyName name,
         String is_multi_valued) throws UnableToCompleteException {
       boolean isMultiValued = toPrimitiveBoolean(is_multi_valued);
@@ -1014,6 +1068,58 @@
     }
   }
 
+  private static class PropertyValueGlob {
+    public final String token;
+
+    public PropertyValueGlob(String token) {
+      this.token = token;
+    }
+
+    public boolean isGlob() {
+      return token.contains(BindingProperty.GLOB_STAR);
+    }
+  }
+
+  /**
+   * Converts a comma-separated string into an array of property value tokens.
+   */
+  private final class PropertyValueGlobArrayAttrCvt extends AttributeConverter {
+    public Object convertToArg(Schema schema, int line, String elem,
+        String attr, String value) throws UnableToCompleteException {
+      String[] tokens = value.split(",");
+      PropertyValueGlob[] values = new PropertyValueGlob[tokens.length];
+
+      // Validate each token as we copy it over.
+      //
+      for (int i = 0; i < tokens.length; i++) {
+        values[i] = (PropertyValueGlob) propValueGlobAttrCvt.convertToArg(
+            schema, line, elem, attr, tokens[i]);
+      }
+
+      return values;
+    }
+  }
+
+  /**
+   * Converts a string into a property value glob, validating it in the process.
+   */
+  private final class PropertyValueGlobAttrCvt extends AttributeConverter {
+    public Object convertToArg(Schema schema, int line, String elem,
+        String attr, String value) throws UnableToCompleteException {
+
+      String token = value.trim();
+      String tokenNoStar = token.replaceAll(
+          Pattern.quote(BindingProperty.GLOB_STAR), "");
+      if (BindingProperty.GLOB_STAR.equals(token)
+          || Util.isValidJavaIdent(tokenNoStar)) {
+        return new PropertyValueGlob(token);
+      } else {
+        Messages.PROPERTY_VALUE_INVALID.log(logger, token, null);
+        throw new UnableToCompleteException();
+      }
+    }
+  }
+
   private static class ScriptSchema extends Schema {
 
     private StringBuffer script;
@@ -1087,6 +1193,8 @@
   private final PropertyNameAttrCvt propNameAttrCvt = new PropertyNameAttrCvt();
   private final PropertyValueArrayAttrCvt propValueArrayAttrCvt = new PropertyValueArrayAttrCvt();
   private final PropertyValueAttrCvt propValueAttrCvt = new PropertyValueAttrCvt();
+  private final PropertyValueGlobArrayAttrCvt propValueGlobArrayAttrCvt = new PropertyValueGlobArrayAttrCvt();
+  private final PropertyValueGlobAttrCvt propValueGlobAttrCvt = new PropertyValueGlobAttrCvt();
 
   public ModuleDefSchema(TreeLogger logger, ModuleDefLoader loader,
       String moduleName, URL moduleURL, String modulePackageAsPath,
@@ -1106,6 +1214,9 @@
         configurationPropAttrCvt);
     registerAttributeConverter(PropertyValue.class, propValueAttrCvt);
     registerAttributeConverter(PropertyValue[].class, propValueArrayAttrCvt);
+    registerAttributeConverter(PropertyValueGlob.class, propValueGlobAttrCvt);
+    registerAttributeConverter(PropertyValueGlob[].class,
+        propValueGlobArrayAttrCvt);
     registerAttributeConverter(LinkerName.class, linkerNameAttrCvt);
     registerAttributeConverter(NullableName.class, nullableNameAttrCvt);
     registerAttributeConverter(Class.class, classAttrCvt);
diff --git a/dev/core/src/com/google/gwt/dev/cfg/PropertyPermutations.java b/dev/core/src/com/google/gwt/dev/cfg/PropertyPermutations.java
index be1a05a..48dfef1 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/PropertyPermutations.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/PropertyPermutations.java
@@ -18,6 +18,7 @@
 import com.google.gwt.core.ext.PropertyOracle;
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.util.CollapsedPropertyKey;
 
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -26,6 +27,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
 
 /**
  * Generates all possible permutations of properties in a module. Each
@@ -161,6 +164,52 @@
     values = allPermutations.values.subList(firstPerm, firstPerm + numPerms);
   }
 
+  /**
+   * Copy constructor that allows the list of property values to be reset.
+   */
+  public PropertyPermutations(PropertyPermutations allPermutations,
+      List<String[]> values) {
+    this.properties = allPermutations.properties;
+    this.values = values;
+  }
+
+  /**
+   * Return a list of PropertyPermutations that represent the hard permutations
+   * that result from collapsing the soft properties in the
+   * PropertyPermutation's Properties object.
+   */
+  public List<PropertyPermutations> collapseProperties() {
+    // Collate property values in this map
+    SortedMap<CollapsedPropertyKey, List<String[]>> map = new TreeMap<CollapsedPropertyKey, List<String[]>>();
+
+    // Loop over all possible property value permutations
+    for (Iterator<String[]> it = iterator(); it.hasNext();) {
+      String[] propertyValues = it.next();
+      assert propertyValues.length == getOrderedProperties().length;
+
+      StaticPropertyOracle oracle = new StaticPropertyOracle(
+          getOrderedProperties(), propertyValues, new ConfigurationProperty[0]);
+      CollapsedPropertyKey key = new CollapsedPropertyKey(
+          oracle);
+
+      List<String[]> list = map.get(key);
+      if (list == null) {
+        list = new ArrayList<String[]>();
+        map.put(key, list);
+      }
+      list.add(propertyValues);
+    }
+
+    // Return the collated values
+    List<PropertyPermutations> toReturn = new ArrayList<PropertyPermutations>(
+        map.size());
+    for (List<String[]> list : map.values()) {
+      toReturn.add(new PropertyPermutations(this, list));
+    }
+
+    return toReturn;
+  }
+
   public BindingProperty[] getOrderedProperties() {
     return getOrderedPropertiesOf(properties);
   }
diff --git a/dev/core/src/com/google/gwt/dev/cfg/StaticPropertyOracle.java b/dev/core/src/com/google/gwt/dev/cfg/StaticPropertyOracle.java
index 3b57240..b7d2e7b 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/StaticPropertyOracle.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/StaticPropertyOracle.java
@@ -158,4 +158,17 @@
 
     throw new BadPropertyValueException(propertyName);
   }
+  
+  /**
+   * Dumps the binding property key/value pairs; For debugging use only.
+   */
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0, j = orderedProps.length; i < j; i++) {
+      sb.append(orderedProps[i].getName()).append(" = ").append(
+          orderedPropValues[i]).append(" ");
+    }
+    return sb.toString();
+  }
 }
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 1fcdba2..bf07384 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -220,7 +220,6 @@
     InternalCompilerException.preload();
     PropertyOracle[] propertyOracles = permutation.getPropertyOracles();
     int permutationId = permutation.getId();
-    Map<String, String> rebindAnswers = permutation.getRebindAnswers();
     logger.log(TreeLogger.INFO, "Compiling permutation " + permutationId
         + "...");
     long permStart = System.currentTimeMillis();
@@ -238,7 +237,7 @@
       Map<StandardSymbolData, JsName> symbolTable = new TreeMap<StandardSymbolData, JsName>(
           new SymbolData.ClassIdentComparator());
 
-      ResolveRebinds.exec(jprogram, rebindAnswers);
+      ResolveRebinds.exec(jprogram, permutation.getOrderedRebindAnswers());
 
       // (4) Optimize the normalized Java AST for each permutation.
       if (options.isDraftCompile()) {
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 59b74c5..9914d57 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
@@ -65,6 +65,7 @@
   public static final Set<String> CODEGEN_TYPES_SET = new LinkedHashSet<String>(
       Arrays.asList(new String[] {
           "com.google.gwt.lang.Array", "com.google.gwt.lang.Cast",
+          "com.google.gwt.lang.CollapsedPropertyHolder",
           "com.google.gwt.lang.Exceptions", "com.google.gwt.lang.LongLib",
           "com.google.gwt.lang.Stats",}));
 
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 dae6575..95863a2 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
@@ -1468,9 +1468,10 @@
       /**
        * <pre>
        * var $entry = Impl.registerEntry();
-       * function gwtOnLoad(errFn, modName, modBase){
+       * function gwtOnLoad(errFn, modName, modBase, softPermutationId){
        *   $moduleName = modName;
        *   $moduleBase = modBase;
+       *   CollapsedPropertyHolder.permutationId = softPermutationId;
        *   if (errFn) {
        *     try {
        *       $entry(init)();
@@ -1510,9 +1511,11 @@
       JsName errFn = fnScope.declareName("errFn");
       JsName modName = fnScope.declareName("modName");
       JsName modBase = fnScope.declareName("modBase");
+      JsName softPermutationId = fnScope.declareName("softPermutationId");
       params.add(new JsParameter(sourceInfo, errFn));
       params.add(new JsParameter(sourceInfo, modName));
       params.add(new JsParameter(sourceInfo, modBase));
+      params.add(new JsParameter(sourceInfo, softPermutationId));
       JsExpression asg = createAssignment(
           topScope.findExistingUnobfuscatableName("$moduleName").makeRef(
               sourceInfo), modName.makeRef(sourceInfo));
@@ -1520,6 +1523,15 @@
       asg = createAssignment(topScope.findExistingUnobfuscatableName(
           "$moduleBase").makeRef(sourceInfo), modBase.makeRef(sourceInfo));
       body.getStatements().add(asg.makeStmt());
+
+      // Assignment to CollapsedPropertyHolder.permutationId only if it's used
+      JsName permutationIdFieldName = names.get(program.getIndexedField("CollapsedPropertyHolder.permutationId"));
+      if (permutationIdFieldName != null) {
+        asg = createAssignment(permutationIdFieldName.makeRef(sourceInfo),
+            softPermutationId.makeRef(sourceInfo));
+        body.getStatements().add(asg.makeStmt());
+      }
+
       JsIf jsIf = new JsIf(sourceInfo);
       body.getStatements().add(jsIf);
       jsIf.setIfExpr(errFn.makeRef(sourceInfo));
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/ResolveRebinds.java b/dev/core/src/com/google/gwt/dev/jjs/impl/ResolveRebinds.java
index 048a7b0..373c2bd 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/ResolveRebinds.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/ResolveRebinds.java
@@ -16,15 +16,31 @@
 package com.google.gwt.dev.jjs.impl;
 
 import com.google.gwt.dev.jjs.InternalCompilerException;
+import com.google.gwt.dev.jjs.SourceInfo;
 import com.google.gwt.dev.jjs.ast.Context;
+import com.google.gwt.dev.jjs.ast.JBlock;
+import com.google.gwt.dev.jjs.ast.JCaseStatement;
 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.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.JModVisitor;
 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.JReturnStatement;
+import com.google.gwt.dev.jjs.ast.JSwitchStatement;
 import com.google.gwt.dev.jjs.ast.JType;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -37,6 +53,15 @@
   private class RebindVisitor extends JModVisitor {
     @Override
     public void endVisit(JGwtCreate x, Context ctx) {
+
+      if (isSoftRebind(x.getSourceType())) {
+        JMethod method = rebindMethod(x.getSourceInfo(), x.getSourceType(),
+            x.getResultTypes(), x.getInstantiationExpressions());
+        JMethodCall call = new JMethodCall(x.getSourceInfo(), null, method);
+        ctx.replaceMe(call);
+        return;
+      }
+
       JClassType rebindResult = rebind(x.getSourceType());
       List<JClassType> rebindResults = x.getResultTypes();
       for (int i = 0; i < rebindResults.size(); ++i) {
@@ -53,6 +78,15 @@
 
     @Override
     public void endVisit(JReboundEntryPoint x, Context ctx) {
+
+      if (isSoftRebind(x.getSourceType())) {
+        JMethod method = rebindMethod(x.getSourceInfo(), x.getSourceType(),
+            x.getResultTypes(), x.getEntryCalls());
+        JMethodCall call = new JMethodCall(x.getSourceInfo(), null, method);
+        ctx.replaceMe(call.makeStatement());
+        return;
+      }
+
       JClassType rebindResult = rebind(x.getSourceType());
       List<JClassType> rebindResults = x.getResultTypes();
       for (int i = 0; i < rebindResults.size(); ++i) {
@@ -68,22 +102,52 @@
     }
   }
 
-  public static boolean exec(JProgram program, Map<String, String> rebindAnswers) {
-    return new ResolveRebinds(program, rebindAnswers).execImpl();
+  public static boolean exec(JProgram program,
+      Map<String, String>[] orderedRebindAnswers) {
+    return new ResolveRebinds(program, orderedRebindAnswers).execImpl();
   }
 
-  private final JProgram program;
-  private final Map<String, String> rebindAnswers;
+  /**
+   * Returns the rebind answers that do not vary across various maps of rebind
+   * answers.
+   */
+  public static Map<String, String> getHardRebindAnswers(
+      Map<String, String>[] rebindAnswers) {
+    Iterator<Map<String, String>> it = Arrays.asList(rebindAnswers).iterator();
 
-  private ResolveRebinds(JProgram program, Map<String, String> rebindAnswers) {
+    // Start with an arbitrary copy of a rebind answer map
+    Map<String, String> toReturn = new HashMap<String, String>(it.next());
+
+    while (it.hasNext()) {
+      Map<String, String> next = it.next();
+      // Only keep key/value pairs present in the other rebind map
+      toReturn.entrySet().retainAll(next.entrySet());
+    }
+
+    return toReturn;
+  }
+
+  private final Map<String, String> hardRebindAnswers;
+  private final JClassType holderType;
+  private final Map<String, String>[] orderedRebindAnswers;
+  private final JMethod permutationIdMethod;
+  private final JProgram program;
+  private final Map<JReferenceType, JMethod> rebindMethods = new IdentityHashMap<JReferenceType, JMethod>();
+
+  private ResolveRebinds(JProgram program,
+      Map<String, String>[] orderedRebindAnswers) {
     this.program = program;
-    this.rebindAnswers = rebindAnswers;
+    this.orderedRebindAnswers = orderedRebindAnswers;
+
+    this.hardRebindAnswers = getHardRebindAnswers(orderedRebindAnswers);
+    this.holderType = (JClassType) program.getIndexedType("CollapsedPropertyHolder");
+    this.permutationIdMethod = program.getIndexedMethod("CollapsedPropertyHolder.getPermutationId");
   }
 
   public JClassType rebind(JType type) {
     // Rebinds are always on a source type name.
     String reqType = type.getName().replace('$', '.');
-    String reboundClassName = rebindAnswers.get(reqType);
+    String reboundClassName = hardRebindAnswers.get(reqType);
     if (reboundClassName == null) {
       // The fact that we already compute every rebind permutation before
       // compiling should prevent this case from ever happening in real life.
@@ -102,4 +166,109 @@
     return rebinder.didChange();
   }
 
+  private boolean isSoftRebind(JType type) {
+    String reqType = type.getName().replace('$', '.');
+    return !hardRebindAnswers.containsKey(reqType);
+  }
+
+  private JMethod rebindMethod(SourceInfo info, JReferenceType requestType,
+      List<JClassType> resultTypes, List<JExpression> instantiationExpressions) {
+    assert resultTypes.size() == instantiationExpressions.size();
+
+    JMethod toReturn = rebindMethods.get(requestType);
+    if (toReturn != null) {
+      return toReturn;
+    }
+
+    info = info.makeChild(ResolveRebinds.class, "Rebind factory for "
+        + requestType.getName());
+
+    // Maps the result types to the various virtual permutation ids
+    Map<JClassType, List<Integer>> resultsToPermutations = new LinkedHashMap<JClassType, List<Integer>>();
+
+    for (int i = 0, j = orderedRebindAnswers.length; i < j; i++) {
+      Map<String, String> answerMap = orderedRebindAnswers[i];
+      String answerTypeName = answerMap.get(requestType.getName().replace('$',
+          '.'));
+      // We take an answer class, e.g. DOMImplSafari ...
+      JClassType answerType = (JClassType) program.getFromTypeMap(answerTypeName);
+
+      List<Integer> list = resultsToPermutations.get(answerType);
+      if (list == null) {
+        list = new ArrayList<Integer>();
+        resultsToPermutations.put(answerType, list);
+      }
+      // and map it to the permutation ID for a particular set of values
+      list.add(i);
+    }
+
+    // Pick the most-used result type to emit less code
+    JClassType mostUsed = null;
+    {
+      int max = 0;
+      for (Map.Entry<JClassType, List<Integer>> entry : resultsToPermutations.entrySet()) {
+        int size = entry.getValue().size();
+        if (size > max) {
+          max = size;
+          mostUsed = entry.getKey();
+        }
+      }
+    }
+    assert mostUsed != null;
+
+    // c_g_g_d_c_i_DOMImpl
+    toReturn = program.createMethod(info, requestType.getName().replace("_",
+        "_1").replace('.', '_').toCharArray(), holderType,
+        program.getNonNullType(program.getTypeJavaLangObject()), false, true,
+        true, false, false);
+    toReturn.freezeParamTypes();
+    rebindMethods.put(requestType, toReturn);
+
+    // Used in the return statement at the end
+    JExpression mostUsedExpression = null;
+
+    JBlock switchBody = new JBlock(info);
+    for (int i = 0, j = resultTypes.size(); i < j; i++) {
+      JClassType resultType = resultTypes.get(i);
+      JExpression instantiation = instantiationExpressions.get(i);
+
+      List<Integer> permutations = resultsToPermutations.get(resultType);
+      if (permutations == null) {
+        // This rebind result is unused in this permutation
+        continue;
+      } else if (resultType == mostUsed) {
+        // Save off the fallback expression and go onto the next type
+        mostUsedExpression = instantiation;
+        continue;
+      }
+
+      for (int permutationId : permutations) {
+        // case 33:
+        switchBody.addStmt(new JCaseStatement(info,
+            program.getLiteralInt(permutationId)));
+      }
+
+      // return new FooImpl();
+      JReturnStatement ret = new JReturnStatement(info, instantiation);
+      switchBody.addStmt(ret);
+    }
+
+    assert switchBody.getStatements().size() > 0 : "No case statement emitted "
+        + "for supposedly soft-rebind type " + requestType.getName();
+
+    // switch (CollapsedPropertyHolder.getPermutationId()) { ... }
+    JSwitchStatement sw = new JSwitchStatement(info, new JMethodCall(info,
+        null, permutationIdMethod), switchBody);
+
+    // return new FallbackImpl(); at the very end.
+    assert mostUsedExpression != null : "No most-used expression";
+    JReturnStatement fallbackReturn = new JReturnStatement(info,
+        mostUsedExpression);
+
+    JMethodBody body = (JMethodBody) toReturn.getBody();
+    body.getBlock().addStmt(sw);
+    body.getBlock().addStmt(fallbackReturn);
+
+    return toReturn;
+  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/util/CollapsedPropertyKey.java b/dev/core/src/com/google/gwt/dev/util/CollapsedPropertyKey.java
new file mode 100644
index 0000000..a942306
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/CollapsedPropertyKey.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2010 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.util;
+
+import com.google.gwt.dev.Permutation;
+import com.google.gwt.dev.cfg.BindingProperty;
+import com.google.gwt.dev.cfg.StaticPropertyOracle;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * Creates a string representation of the binding property key/value pairs used
+ * in a Permutation. The value of a collapsed property will be represented by
+ * the set of equivalent values.
+ * <p>
+ * Assume that the <code>safari</code> and <code>ie8</code>
+ * <code>user.agent</code> values have been collapsed. Instead of printing
+ * <code>user.agent=safari</code>, this class will use
+ * <code>user.agent = { ie8, safari }</code>.
+ */
+public class CollapsedPropertyKey extends StringKey {
+  /**
+   * Create the string key for a collection of property oracles.
+   */
+  private static String collapse(StaticPropertyOracle... oracles) {
+    // The map used to create the string key
+    SortedMap<String, SortedSet<String>> collapsedPropertyMap = new TreeMap<String, SortedSet<String>>();
+    for (StaticPropertyOracle oracle : oracles) {
+      for (int i = 0, j = oracle.getOrderedProps().length; i < j; i++) {
+        BindingProperty prop = oracle.getOrderedProps()[i];
+        String value = oracle.getOrderedPropValues()[i];
+        boolean isCollapsed = false;
+
+        // Iterate over the equivalence sets defined in the property
+        for (Set<String> equivalenceSet : prop.getCollapsedValues()) {
+          if (equivalenceSet.contains(value)) {
+            /*
+             * If we find a set that contains the current value, add all the
+             * values in the set. This accounts for the transitive nature of
+             * equality.
+             */
+            SortedSet<String> toAdd = collapsedPropertyMap.get(prop.getName());
+            if (toAdd == null) {
+              toAdd = new TreeSet<String>();
+              collapsedPropertyMap.put(prop.getName(), toAdd);
+              isCollapsed = true;
+            }
+            toAdd.addAll(equivalenceSet);
+          }
+        }
+        if (!isCollapsed) {
+          // For "hard" properties, add the singleton value
+          collapsedPropertyMap.put(prop.getName(), new TreeSet<String>(
+              Arrays.asList(value)));
+        }
+      }
+    }
+    return collapsedPropertyMap.toString();
+  }
+
+  private final Permutation permutation;
+
+  /**
+   * Constructor that constructs a key containing all collapsed property/value
+   * pairs used by a Permutation. The given Permutation can be retrieved later
+   * through {@link #getPermutation()}.
+   */
+  public CollapsedPropertyKey(Permutation permutation) {
+    super(collapse(permutation.getPropertyOracles()));
+    this.permutation = permutation;
+  }
+
+  /**
+   * Constructor that constructs a key based on all collapsed property/value
+   * pairs defined by the given property oracle.
+   */
+  public CollapsedPropertyKey(StaticPropertyOracle oracle) {
+    super(collapse(oracle));
+    this.permutation = null;
+  }
+
+  public Permutation getPermutation() {
+    return permutation;
+  }
+}
\ No newline at end of file
diff --git a/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/CollapsedPropertyHolder.java b/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/CollapsedPropertyHolder.java
new file mode 100644
index 0000000..a14ba9b
--- /dev/null
+++ b/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/CollapsedPropertyHolder.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010 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.lang;
+
+/**
+ * This is a magic class the compiler uses to contain synthetic methods to
+ * support collapsed or "soft" properties.
+ */
+final class CollapsedPropertyHolder {
+
+  /**
+   * This variable is initialized by the compiler in gwtOnLoad.
+   */
+  public static volatile int permutationId = -1;
+
+  public static int getPermutationId() {
+    assert permutationId != -1 : "The bootstrap linker did not provide a "
+        + "soft permutation id to the gwtOnLoad function";
+    return permutationId;
+  }
+}
diff --git a/dev/core/test/com/google/gwt/dev/cfg/ModuleDefTest.java b/dev/core/test/com/google/gwt/dev/cfg/ModuleDefTest.java
index cd3baab..5be688c 100644
--- a/dev/core/test/com/google/gwt/dev/cfg/ModuleDefTest.java
+++ b/dev/core/test/com/google/gwt/dev/cfg/ModuleDefTest.java
@@ -28,6 +28,8 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
+import java.util.SortedSet;
 
 /**
  * Runs tests directly on ModuleDef.
@@ -84,6 +86,59 @@
   static class FakeLinkerPrimary2 extends FakeLinker {
   }
 
+  public void testCollapsedProperties() {
+    ModuleDef def = new ModuleDef("fake");
+
+    Properties p = def.getProperties();
+    BindingProperty b = p.createBinding("fake");
+    b.addDefinedValue(b.getRootCondition(), "a");
+    b.addDefinedValue(b.getRootCondition(), "b");
+    b.addDefinedValue(b.getRootCondition(), "c");
+    b.addDefinedValue(b.getRootCondition(), "d");
+    b.addDefinedValue(b.getRootCondition(), "e");
+    b.addDefinedValue(b.getRootCondition(), "f1");
+    b.addDefinedValue(b.getRootCondition(), "f2");
+    b.addDefinedValue(b.getRootCondition(), "f3");
+    b.addDefinedValue(b.getRootCondition(), "g1a");
+    b.addDefinedValue(b.getRootCondition(), "g2a");
+    b.addDefinedValue(b.getRootCondition(), "g1b");
+    b.addDefinedValue(b.getRootCondition(), "g2b");
+
+    // Check de-duplication
+    b.addCollapsedValues("a", "b");
+    b.addCollapsedValues("b", "a");
+
+    // Check transitivity
+    b.addCollapsedValues("c", "d");
+    b.addCollapsedValues("c", "e");
+
+    // Check globs
+    b.addCollapsedValues("f*");
+    b.addCollapsedValues("g*a");
+
+    b.normalizeCollapsedValues();
+
+    List<SortedSet<String>> collapsedValues = b.getCollapsedValues();
+    assertEquals(4, collapsedValues.size());
+    assertEquals(Arrays.asList("a", "b"), new ArrayList<String>(
+        collapsedValues.get(0)));
+    assertEquals(Arrays.asList("c", "d", "e"), new ArrayList<String>(
+        collapsedValues.get(1)));
+    assertEquals(Arrays.asList("f1", "f2", "f3"), new ArrayList<String>(
+        collapsedValues.get(2)));
+    assertEquals(Arrays.asList("g1a", "g2a"), new ArrayList<String>(
+        collapsedValues.get(3)));
+
+    // Collapse everything
+    b.addCollapsedValues("*");
+    b.normalizeCollapsedValues();
+
+    collapsedValues = b.getCollapsedValues();
+    assertEquals(1, collapsedValues.size());
+    assertEquals(Arrays.asList(b.getDefinedValues()), new ArrayList<String>(
+        collapsedValues.get(0)));
+  }
+
   public void testLinkerOrder() throws UnableToCompleteException {
     ModuleDef def = new ModuleDef("fake");
 
diff --git a/dev/core/test/com/google/gwt/dev/util/test/PropertyPermutationsTest.java b/dev/core/test/com/google/gwt/dev/util/test/PropertyPermutationsTest.java
index de54d84..2c06394 100644
--- a/dev/core/test/com/google/gwt/dev/util/test/PropertyPermutationsTest.java
+++ b/dev/core/test/com/google/gwt/dev/util/test/PropertyPermutationsTest.java
@@ -24,7 +24,9 @@
 
 import junit.framework.TestCase;
 
+import java.util.Arrays;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -90,6 +92,38 @@
     assertEquals("true", perm[0]);
   }
 
+  public void testOneDimensionPermWithCollapse() {
+    ModuleDef md = new ModuleDef("testOneDimensionPerm");
+    Properties props = md.getProperties();
+
+    {
+      BindingProperty prop = props.createBinding("debug");
+      prop.addDefinedValue(prop.getRootCondition(), "false");
+      prop.addDefinedValue(prop.getRootCondition(), "true");
+      prop.addCollapsedValues("true", "false");
+    }
+
+    // Permutations and their values are in stable alphabetical order.
+    //
+    PropertyPermutations perms = new PropertyPermutations(md.getProperties(),
+        md.getActiveLinkerNames());
+
+    List<PropertyPermutations> collapsed = perms.collapseProperties();
+    assertEquals("size", 1, collapsed.size());
+    perms = collapsed.get(0);
+
+    String[] perm;
+    Iterator<String[]> iter = perms.iterator();
+
+    assertTrue(iter.hasNext());
+    perm = iter.next();
+    assertEquals("false", perm[0]);
+
+    assertTrue(iter.hasNext());
+    perm = iter.next();
+    assertEquals("true", perm[0]);
+  }
+
   public void testTwoDimensionPerm() {
     ModuleDef md = new ModuleDef("testTwoDimensionPerm");
     Properties props = md.getProperties();
@@ -145,6 +179,45 @@
     assertEquals("opera", perm[1]);
   }
 
+  public void testTwoDimensionPermWithCollapse() {
+    ModuleDef md = new ModuleDef("testTwoDimensionPerm");
+    Properties props = md.getProperties();
+
+    {
+      BindingProperty prop = props.createBinding("user.agent");
+      prop.addDefinedValue(prop.getRootCondition(), "moz");
+      prop.addDefinedValue(prop.getRootCondition(), "ie6");
+      prop.addDefinedValue(prop.getRootCondition(), "opera");
+      prop.addCollapsedValues("moz", "ie6", "opera");
+    }
+
+    {
+      BindingProperty prop = props.createBinding("debug");
+      prop.addDefinedValue(prop.getRootCondition(), "false");
+      prop.addDefinedValue(prop.getRootCondition(), "true");
+    }
+
+    // String[]s and their values are in stable alphabetical order.
+    //
+    PropertyPermutations perms = new PropertyPermutations(md.getProperties(),
+        md.getActiveLinkerNames());
+
+    List<PropertyPermutations> collapsed = perms.collapseProperties();
+    assertEquals("size", 2, collapsed.size());
+
+    Iterator<String[]> it = collapsed.get(0).iterator();
+    assertEquals(Arrays.asList("false", "ie6"), Arrays.asList(it.next()));
+    assertEquals(Arrays.asList("false", "moz"), Arrays.asList(it.next()));
+    assertEquals(Arrays.asList("false", "opera"), Arrays.asList(it.next()));
+    assertFalse(it.hasNext());
+
+    it = collapsed.get(1).iterator();
+    assertEquals(Arrays.asList("true", "ie6"), Arrays.asList(it.next()));
+    assertEquals(Arrays.asList("true", "moz"), Arrays.asList(it.next()));
+    assertEquals(Arrays.asList("true", "opera"), Arrays.asList(it.next()));
+    assertFalse(it.hasNext());
+  }
+
   public void testTwoDimensionPermWithExpansion() {
     ModuleDef md = new ModuleDef("testTwoDimensionsWithExpansion");
     Properties props = md.getProperties();
diff --git a/distro-source/core/src/gwt-module.dtd b/distro-source/core/src/gwt-module.dtd
index 7916161..fd59093 100644
--- a/distro-source/core/src/gwt-module.dtd
+++ b/distro-source/core/src/gwt-module.dtd
@@ -134,6 +134,17 @@
 	name CDATA #REQUIRED
 	values CDATA #REQUIRED
 >
+<!-- Collapse property values to produce soft permutations -->
+<!ELEMENT collapse-property EMPTY>
+<!ATTLIST collapse-property
+	name CDATA #REQUIRED
+	value CDATA #REQUIRED
+>
+<!-- Collapse all deferred-binding properties to produce a single permutation -->
+<!ELEMENT collapse-all-properties EMPTY>
+<!ATTLIST collapse-all-properties
+	value (true | false) "true"
+>
 <!-- Add additional allowable values to a configuration property -->
 <!ELEMENT extend-configuration-property EMPTY>
 <!ATTLIST extend-configuration-property
diff --git a/user/src/com/google/gwt/junit/JUnit.gwt.xml b/user/src/com/google/gwt/junit/JUnit.gwt.xml
index df4eadb..8963a34 100644
--- a/user/src/com/google/gwt/junit/JUnit.gwt.xml
+++ b/user/src/com/google/gwt/junit/JUnit.gwt.xml
@@ -42,4 +42,6 @@
 
   <inherits name="com.google.gwt.benchmarks.Benchmarks"/>
 
+  <!-- Speed up test compilations by producing one permutation -->
+  <collapse-all-properties />
 </module>
diff --git a/user/test/com/google/gwt/core/ext/linker/impl/SelectionScriptLinkerUnitTest.java b/user/test/com/google/gwt/core/ext/linker/impl/SelectionScriptLinkerUnitTest.java
index e6085cd..a1b3bf4 100644
--- a/user/test/com/google/gwt/core/ext/linker/impl/SelectionScriptLinkerUnitTest.java
+++ b/user/test/com/google/gwt/core/ext/linker/impl/SelectionScriptLinkerUnitTest.java
@@ -34,6 +34,7 @@
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.SortedSet;
 import java.util.TreeMap;
 import java.util.TreeSet;
@@ -179,6 +180,7 @@
     StandardCompilationResult result = new StandardCompilationResult(
         new MockPermutationResult());
     result.addSelectionPermutation(new TreeMap<SelectionProperty, String>());
+    result.addSoftPermutation(Collections.<SelectionProperty, String> emptyMap());
     artifacts.add(result);
 
     ArtifactSet updated = new NonShardableSelectionScriptLinker().link(