Pretty massive rewrite of PrivateModules so that they're now implemented in core Guice.

This change introduces InjectorShell, an unfortunate class to hold an in-progress injector while it's being constructed. It refactors InjectorBuilder to support building several injectors simultaneously.

Still outstanding is fixing up the docs for PrivateModule and these new APIs.

git-svn-id: https://google-guice.googlecode.com/svn/trunk@701 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/extensions/assistedinject/test/com/google/inject/assistedinject/FactoryProviderTest.java b/extensions/assistedinject/test/com/google/inject/assistedinject/FactoryProviderTest.java
index ddbf72f..7f649f4 100755
--- a/extensions/assistedinject/test/com/google/inject/assistedinject/FactoryProviderTest.java
+++ b/extensions/assistedinject/test/com/google/inject/assistedinject/FactoryProviderTest.java
@@ -18,6 +18,7 @@
 
 import com.google.inject.AbstractModule;
 import static com.google.inject.Asserts.assertContains;
+import com.google.inject.ConfigurationException;
 import com.google.inject.CreationException;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
@@ -170,8 +171,8 @@
     try {
       FactoryProvider.newFactory(SummerCarFactory.class, Beetle.class);
       fail();
-    } catch(IllegalArgumentException e) {
-      assertTrue(e.getMessage().startsWith("Constructor mismatch"));
+    } catch(ConfigurationException e) {
+      assertContains(e.getMessage(), "Constructor mismatch");
     }
   }
 
@@ -379,7 +380,7 @@
     try {
       FactoryProvider.newFactory(DefectiveCarFactoryWithNoExceptions.class, DefectiveCar.class);
       fail();
-    } catch (IllegalStateException expected) {
+    } catch (ConfigurationException expected) {
       assertContains(expected.getMessage(), "no compatible exception is thrown");
     }
   }
diff --git a/extensions/commands/src/com/google/inject/commands/intercepting/InterceptingInjectorBuilder.java b/extensions/commands/src/com/google/inject/commands/intercepting/InterceptingInjectorBuilder.java
index b59f84b..decd8e2 100644
--- a/extensions/commands/src/com/google/inject/commands/intercepting/InterceptingInjectorBuilder.java
+++ b/extensions/commands/src/com/google/inject/commands/intercepting/InterceptingInjectorBuilder.java
@@ -31,7 +31,6 @@
 import com.google.inject.ProvidedBy;
 import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
-import com.google.inject.binder.LinkedBindingBuilder;
 import com.google.inject.binder.ScopedBindingBuilder;
 import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.name.Names;
@@ -175,8 +174,7 @@
       Key<T> anonymousKey = Key.get(key.getTypeLiteral(), UniqueAnnotations.create());
       binder.bind(key).toProvider(new InterceptingProvider<T>(key, anonymousKey));
 
-      LinkedBindingBuilder<T> linkedBindingBuilder = binder.bind(anonymousKey);
-      ScopedBindingBuilder scopedBindingBuilder = applyTarget(binding, linkedBindingBuilder);
+      ScopedBindingBuilder scopedBindingBuilder = bindKeyToTarget(binding, binder, anonymousKey);
 
       // we scope the user's provider, not the interceptor. This is dangerous,
       // but convenient. It means that although the user's provider will live
diff --git a/extensions/privatemodules/src/com/google/inject/privatemodules/PrivateModule.java b/extensions/privatemodules/src/com/google/inject/privatemodules/PrivateModule.java
deleted file mode 100644
index ecc2821..0000000
--- a/extensions/privatemodules/src/com/google/inject/privatemodules/PrivateModule.java
+++ /dev/null
@@ -1,393 +0,0 @@
-/**
- * Copyright (C) 2008 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.inject.privatemodules;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
-import com.google.common.collect.Sets;
-import com.google.inject.Binder;
-import com.google.inject.Binding;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Key;
-import com.google.inject.Module;
-import com.google.inject.Provider;
-import com.google.inject.Scope;
-import com.google.inject.Stage;
-import com.google.inject.TypeLiteral;
-import com.google.inject.binder.AnnotatedBindingBuilder;
-import com.google.inject.binder.AnnotatedConstantBindingBuilder;
-import com.google.inject.binder.LinkedBindingBuilder;
-import com.google.inject.internal.ProviderMethod;
-import com.google.inject.internal.ProviderMethodsModule;
-import com.google.inject.internal.SourceProvider;
-import com.google.inject.internal.UniqueAnnotations;
-import com.google.inject.matcher.Matcher;
-import com.google.inject.spi.DefaultElementVisitor;
-import com.google.inject.spi.Element;
-import com.google.inject.spi.ElementVisitor;
-import com.google.inject.spi.Elements;
-import com.google.inject.spi.Message;
-import com.google.inject.spi.ModuleWriter;
-import com.google.inject.spi.TypeConverter;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-import java.util.List;
-import java.util.Set;
-import org.aopalliance.intercept.MethodInterceptor;
-
-/**
- * A module whose configuration information is hidden from its environment by default. Only bindings
- * that are explicitly exposed will be available to other modules and to the users of the injector.
- * This module may expose the bindings it creates and the bindings of the modules it installs.
- *
- * <p>A private module can be nested within a regular module or within another private module using
- * {@link Binder#install install()}.  Its bindings live in a new environment that inherits bindings,
- * type converters, scopes, and interceptors from the surrounding ("parent") environment.  When you
- * nest multiple private modules, the result is a tree of environments where the injector's
- * environment is the root.
- *
- * <p>Guice EDSL bindings can be exposed with {@link #expose(Class) expose()}. {@literal @}{@link
- * com.google.inject.Provides Provides} bindings can be exposed with the {@literal @}{@link
- * Exposed} annotation:
- *
- * <pre>
- * public class FooBarBazModule extends PrivateModule {
- *   protected void configurePrivateBindings() {
- *     bind(Foo.class).to(RealFoo.class);
- *     expose(Foo.class);
- *
- *     install(new TransactionalBarModule());
- *     expose(Bar.class).annotatedWith(Transactional.class);
- *
- *     bind(SomeImplementationDetail.class);
- *     install(new MoreImplementationDetailsModule());
- *   }
- *
- *   {@literal @}Provides {@literal @}Exposed
- *   public Baz provideBaz() {
- *     return new SuperBaz();
- *   }
- * }
- * </pre>
- *
- * <p>Private modules are implemented using {@link Injector#createChildInjector(Module[]) parent
- * injectors}. When it can satisfy their dependencies, just-in-time bindings will be created in the
- * root environment. Such bindings are shared among all environments in the tree.
- * 
- * <p>The scope of a binding is constrained to its environment. A singleton bound in a private
- * module will be unique to its environment. But a binding for the same type in a different private
- * module will yield a different instance.
- *
- * <p>A shared binding that injects the {@code Injector} gets the root injector, which only has
- * access to bindings in the root environment. An explicit binding that injects the {@code Injector}
- * gets access to all bindings in the child environment.
- *
- * <p>To promote a just-in-time binding to an explicit binding, bind it:
- * <pre>
- *   bind(FooImpl.class);
- * </pre>
- *
- * @author jessewilson@google.com (Jesse Wilson)
- */
-public abstract class PrivateModule implements Module {
-
-  private final SourceProvider sourceProvider
-      = new SourceProvider().plusSkippedClasses(PrivateModule.class);
-
-  /** When this provider returns, the private injector creation has started. */
-  private Provider<Ready> readyProvider;
-
-  /** Keys exposed to the public injector */
-  private Set<Expose> exposes;
-
-  /** Like abstract module, the binder of the current private module */
-  private Binder privateBinder;
-
-  /*
-   * This implementation is complicated in order to satisfy two different roles in one class:
-   *
-   *  1. As a public module (the one that installs this), we bind only the exposed keys. This is the
-   *     role we play first, when configure() is called by the installing binder. It collects the
-   *     exposed keys and their corresponding providers by executing itself as a private module.
-   *
-   *  2. As a private module, we bind everything. This is performed our own indirect re-entrant call
-   *     to configure() via the Elements.getElements() API.
-   *
-   * Throwing further wrenches into the mix:
-   *
-   *  o Provider methods. The ProviderMethodsModule class special cases modules that extend
-   *    PrivateModules to skip them by default. We have our own provider methods backdoor API
-   *    called ProviderMethodsModule.forPrivateModule so provider methods are only applied when
-   *    we're running as a private module. We also need to iterate through the provider methods
-   *    by hand to gather the ones with the @Exposed annotation
-   *
-   *  o Injector creation time. Dependencies can flow freely between child and parent injectors.
-   *    When providers are being exercised, we need to make sure the child injector construction
-   *    has started.
-   */
-  public final synchronized void configure(Binder binder) {
-    // when 'exposes' is null, we're being run for the public injector
-    if (exposes == null) {
-      configurePublicBindings(binder);
-      return;
-    }
-
-    // otherwise we're being run for the private injector
-    checkState(this.privateBinder == null, "Re-entry is not allowed.");
-    privateBinder = binder.skipSources(PrivateModule.class);
-    try {
-      configurePrivateBindings();
-
-      ProviderMethodsModule providerMethods = ProviderMethodsModule.forPrivateModule(this);
-      for (ProviderMethod<?> providerMethod : providerMethods.getProviderMethods(privateBinder)) {
-        providerMethod.configure(privateBinder);
-        if (providerMethod.getMethod().isAnnotationPresent(Exposed.class)) {
-          expose(providerMethod.getKey());
-        }
-      }
-
-      for (Expose<?> expose : exposes) {
-        expose.initPrivateProvider(binder);
-      }
-    } finally {
-      privateBinder = null;
-    }
-  }
-
-
-  private void configurePublicBindings(Binder publicBinder) {
-    exposes = Sets.newLinkedHashSet();
-    Key<Ready> readyKey = Key.get(Ready.class, UniqueAnnotations.create());
-    readyProvider = publicBinder.getProvider(readyKey);
-    try {
-      List<Element> privateElements = Elements.getElements(this); // reentrant on configure()
-      Set<Key<?>> privatelyBoundKeys = getBoundKeys(privateElements);
-      final Module privateModule = new ModuleWriter().create(privateElements);
-
-      for (Expose<?> expose : exposes) {
-        if (!privatelyBoundKeys.contains(expose.key)) {
-          publicBinder.addError("Could not expose() at %s%n %s must be explicitly bound.",
-              expose.source, expose.key);
-        } else {
-          expose.configure(publicBinder);
-        }
-      }
-
-      // create the private injector while the public injector is injecting its members. This is
-      // necessary so the providers from getProvider() will work. We use provider injection as our
-      // hook. Guice promises that initialize() will be called before a Ready is returned.
-      publicBinder.bind(readyKey).toProvider(new Provider<Ready>() {
-        @Inject void initialize(Injector publicInjector) {
-          publicInjector.createChildInjector(privateModule);
-        }
-
-        public Ready get() {
-          return new Ready();
-        }
-      });
-
-    } finally {
-      readyProvider = null;
-      exposes = null;
-    }
-  }
-
-  /** Marker object used to indicate the private injector has been created */
-  private static class Ready {}
-
-  /**
-   * Creates bindings and other configurations private to this module. Use {@link #expose(Class)
-   * expose()} to make the bindings in this module available externally.
-   */
-  protected abstract void configurePrivateBindings();
-
-  /** Makes the binding for {@code key} available to other modules and the injector. */
-  protected final <T> void expose(Key<T> key) {
-    checkState(exposes != null, "Cannot expose %s, private module is not ready", key);
-    exposes.add(new Expose<T>(sourceProvider.get(), readyProvider, key));
-  }
-
-  /**
-   * Makes a binding for {@code type} available to other modules and the injector. Use {@link
-   * ExposedKeyBuilder#annotatedWith(Class) annotatedWith()} to expose {@code type} with a binding
-   * annotation.
-   */
-  protected final <T> ExposedKeyBuilder expose(Class<T> type) {
-    checkState(exposes != null, "Cannot expose %s, private module is not ready", type);
-    Expose<T> expose = new Expose<T>(sourceProvider.get(), readyProvider, Key.get(type));
-    exposes.add(expose);
-    return expose;
-  }
-
-  /**
-   * Makes a binding for {@code type} available to other modules and the injector. Use {@link
-   * ExposedKeyBuilder#annotatedWith(Class) annotatedWith()} to expose {@code type} with a binding
-   * annotation.
-   */
-  protected final <T> ExposedKeyBuilder expose(TypeLiteral<T> type) {
-    checkState(exposes != null, "Cannot expose %s, private module is not ready", type);
-    Expose<T> expose = new Expose<T>(sourceProvider.get(), readyProvider, Key.get(type));
-    exposes.add(expose);
-    return expose;
-  }
-
-  /** Qualifies an exposed type with a binding annotation. */
-  public interface ExposedKeyBuilder {
-    void annotatedWith(Class<? extends Annotation> annotationType);
-    void annotatedWith(Annotation annotation);
-  }
-
-  /** A binding from the private injector make visible to the public injector. */
-  private static class Expose<T> implements ExposedKeyBuilder, Provider<T> {
-    private final Object source;
-    private final Provider<Ready> readyProvider;
-    private Key<T> key; // mutable, a binding annotation may be assigned after exposure creation
-    private Provider<T> privateProvider;
-
-    private Expose(Object source, Provider<Ready> readyProvider, Key<T> key) {
-      this.source = checkNotNull(source, "source");
-      this.readyProvider = checkNotNull(readyProvider, "readyProvider");
-      this.key = checkNotNull(key, "key");
-    }
-
-    public void annotatedWith(Class<? extends Annotation> annotationType) {
-      checkState(key.getAnnotationType() == null, "already annotated");
-      key = Key.get(key.getTypeLiteral(), annotationType);
-    }
-
-    public void annotatedWith(Annotation annotation) {
-      checkState(key.getAnnotationType() == null, "already annotated");
-      key = Key.get(key.getTypeLiteral(), annotation);
-    }
-
-    /** Sets the provider in the private injector, to be used by the public injector */
-    private void initPrivateProvider(Binder privateBinder) {
-      privateProvider = privateBinder.withSource(source).getProvider(key);
-    }
-
-    /** Creates a binding in the public binder */
-    private void configure(Binder publicBinder) {
-      publicBinder.withSource(source).bind(key).toProvider(this);
-    }
-
-    public T get() {
-      readyProvider.get(); // force creation of the private injector
-      return privateProvider.get();
-    }
-  }
-
-  /** Returns the set of keys bound by {@code elements}. */
-  private Set<Key<?>> getBoundKeys(Iterable<? extends Element> elements) {
-    final Set<Key<?>> privatelyBoundKeys = Sets.newHashSet();
-    ElementVisitor<Void> visitor = new DefaultElementVisitor<Void>() {
-      public <T> Void visitBinding(Binding<T> command) {
-        privatelyBoundKeys.add(command.getKey());
-        return null;
-      }
-    };
-
-    for (Element element : elements) {
-      element.acceptVisitor(visitor);
-    }
-
-    return privatelyBoundKeys;
-  }
-
-  // prevent classes migrated from AbstractModule from implementing the wrong method.
-  protected final void configure() {}
-
-  // everything below is copied from AbstractModule
-
-  protected final Binder binder() {
-    return privateBinder;
-  }
-
-  protected final void bindScope(Class<? extends Annotation> scopeAnnotation, Scope scope) {
-    privateBinder.bindScope(scopeAnnotation, scope);
-  }
-
-  protected final <T> LinkedBindingBuilder<T> bind(Key<T> key) {
-    return privateBinder.bind(key);
-  }
-
-  protected final <T> AnnotatedBindingBuilder<T> bind(TypeLiteral<T> typeLiteral) {
-    return privateBinder.bind(typeLiteral);
-  }
-
-  protected final <T> AnnotatedBindingBuilder<T> bind(Class<T> clazz) {
-    return privateBinder.bind(clazz);
-  }
-
-  protected final AnnotatedConstantBindingBuilder bindConstant() {
-    return privateBinder.bindConstant();
-  }
-
-  protected final void install(Module module) {
-    privateBinder.install(module);
-  }
-
-  protected final void addError(String message, Object... arguments) {
-    privateBinder.addError(message, arguments);
-  }
-
-  protected final void addError(Throwable t) {
-    privateBinder.addError(t);
-  }
-
-  protected final void addError(Message message) {
-    privateBinder.addError(message);
-  }
-
-  protected final void requestInjection(Object... objects) {
-    privateBinder.requestInjection(objects);
-  }
-
-  protected final void requestStaticInjection(Class<?>... types) {
-    privateBinder.requestStaticInjection(types);
-  }
-
-  protected final void bindInterceptor(Matcher<? super Class<?>> classMatcher,
-      Matcher<? super Method> methodMatcher, MethodInterceptor... interceptors) {
-    privateBinder.bindInterceptor(classMatcher, methodMatcher, interceptors);
-  }
-
-  protected final void requireBinding(Key<?> key) {
-    privateBinder.getProvider(key);
-  }
-
-  protected final void requireBinding(Class<?> type) {
-    privateBinder.getProvider(type);
-  }
-
-  protected final <T> Provider<T> getProvider(Key<T> key) {
-    return privateBinder.getProvider(key);
-  }
-
-  protected final <T> Provider<T> getProvider(Class<T> type) {
-    return privateBinder.getProvider(type);
-  }
-
-  protected final void convertToTypes(Matcher<? super TypeLiteral<?>> typeMatcher,
-      TypeConverter converter) {
-    privateBinder.convertToTypes(typeMatcher, converter);
-  }
-
-  protected final Stage currentStage() {
-    return privateBinder.currentStage();
-  }
-}
diff --git a/extensions/privatemodules/test/com/google/inject/privatemodules/AllTests.java b/extensions/privatemodules/test/com/google/inject/privatemodules/AllTests.java
index 2d947e8..ba58af0 100644
--- a/extensions/privatemodules/test/com/google/inject/privatemodules/AllTests.java
+++ b/extensions/privatemodules/test/com/google/inject/privatemodules/AllTests.java
@@ -16,6 +16,7 @@
 
 package com.google.inject.privatemodules;
 
+import com.google.inject.PrivateModuleTest;
 import junit.framework.Test;
 import junit.framework.TestSuite;
 
diff --git a/src/com/google/inject/AbstractProcessor.java b/src/com/google/inject/AbstractProcessor.java
index a909f8f..f8269b9 100644
--- a/src/com/google/inject/AbstractProcessor.java
+++ b/src/com/google/inject/AbstractProcessor.java
@@ -19,7 +19,6 @@
 import com.google.inject.internal.Errors;
 import com.google.inject.spi.Element;
 import com.google.inject.spi.ElementVisitor;
-import com.google.inject.spi.Exposure;
 import com.google.inject.spi.InjectionRequest;
 import com.google.inject.spi.InterceptorBinding;
 import com.google.inject.spi.Message;
@@ -43,13 +42,21 @@
 abstract class AbstractProcessor implements ElementVisitor<Boolean> {
 
   protected Errors errors;
+  protected InjectorImpl injector;
 
   protected AbstractProcessor(Errors errors) {
     this.errors = errors;
   }
 
-  public void processCommands(List<Element> elements) {
+  public void process(Iterable<InjectorShell> isolatedInjectorBuilders) {
+    for (InjectorShell injectorShell : isolatedInjectorBuilders) {
+      process(injectorShell.getInjector(), injectorShell.getElements());
+    }
+  }
+
+  public void process(InjectorImpl injector, List<Element> elements) {
     Errors errorsAnyElement = this.errors;
+    this.injector = injector;
     try {
       for (Iterator<Element> i = elements.iterator(); i.hasNext(); ) {
         Element element = i.next();
@@ -61,6 +68,7 @@
       }
     } finally {
       this.errors = errorsAnyElement;
+      this.injector = null;
     }
   }
 
@@ -96,11 +104,7 @@
     return false;
   }
 
-  public Boolean visitPrivateElements(PrivateEnvironment privateEnvironment) {
-    return false;
-  }
-
-  public Boolean visitExposure(Exposure exposure) {
+  public Boolean visitPrivateEnvironment(PrivateEnvironment privateEnvironment) {
     return false;
   }
 }
diff --git a/src/com/google/inject/Binder.java b/src/com/google/inject/Binder.java
index c1634cf..63594f9 100644
--- a/src/com/google/inject/Binder.java
+++ b/src/com/google/inject/Binder.java
@@ -304,8 +304,7 @@
    * @param converter converts values
    * @since 2.0
    */
-  void convertToTypes(Matcher<? super TypeLiteral<?>> typeMatcher,
-      TypeConverter converter);
+  void convertToTypes(Matcher<? super TypeLiteral<?>> typeMatcher, TypeConverter converter);
 
   /**
    * Returns a binder that uses {@code source} as the reference location for
diff --git a/src/com/google/inject/BindingProcessor.java b/src/com/google/inject/BindingProcessor.java
index f51895f..b9442bf 100644
--- a/src/com/google/inject/BindingProcessor.java
+++ b/src/com/google/inject/BindingProcessor.java
@@ -18,16 +18,18 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
+import com.google.inject.ExposedBindingImpl.Factory;
 import com.google.inject.internal.Annotations;
 import com.google.inject.internal.Errors;
 import com.google.inject.internal.ErrorsException;
 import com.google.inject.spi.BindingScopingVisitor;
 import com.google.inject.spi.BindingTargetVisitor;
-import com.google.inject.spi.DefaultBindingTargetVisitor;
 import com.google.inject.spi.InjectionPoint;
+import com.google.inject.spi.PrivateEnvironment;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Constructor;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -38,14 +40,6 @@
  */
 class BindingProcessor extends AbstractProcessor {
 
-  /** Returns the class name of the bound provider, or null */
-  private BindingTargetVisitor<Object, String> GET_BOUND_PROVIDER_CLASS_NAME
-      = new DefaultBindingTargetVisitor<Object, String>() {
-    public String visitProvider(Provider<?> provider, Set<InjectionPoint> injectionPoints) {
-      return provider.getClass().getName();
-    }
-  };
-
   private static final BindingScopingVisitor<LoadStrategy> LOAD_STRATEGY_VISITOR
       = new BindingScopingVisitor<LoadStrategy>() {
     public LoadStrategy visitEagerSingleton() {
@@ -65,18 +59,16 @@
     }
   };
 
-  private final InjectorImpl injector;
-  private final State state;
   private final List<CreationListener> creationListeners = Lists.newArrayList();
   private final Initializer initializer;
-  private final List<Runnable> untargettedBindings = Lists.newArrayList();
+  private final List<Runnable> uninitializedBindings = Lists.newArrayList();
+  private final Map<PrivateEnvironment, InjectorImpl> environmentToInjector;
 
-  BindingProcessor(Errors errors, InjectorImpl injector, State state,
-      Initializer initializer) {
+  BindingProcessor(Errors errors, Initializer initializer,
+      Map<PrivateEnvironment, InjectorImpl> environmentToInjector) {
     super(errors);
-    this.injector = injector;
-    this.state = state;
     this.initializer = initializer;
+    this.environmentToInjector = environmentToInjector;
   }
 
   @Override public <T> Boolean visitBinding(Binding<T> command) {
@@ -108,7 +100,7 @@
       }
 
       public Scope visitScopeAnnotation(Class<? extends Annotation> scopeAnnotation) {
-        Scope scope = state.getScope(scopeAnnotation);
+        Scope scope = injector.state.getScope(scopeAnnotation);
         if (scope != null) {
           return scope;
         } else {
@@ -124,7 +116,8 @@
 
     command.acceptTargetVisitor(new BindingTargetVisitor<T, Void>() {
       public Void visitInstance(T instance, Set<InjectionPoint> injectionPoints) {
-        Initializable<T> ref = initializer.requestInjection(instance, source, injectionPoints);
+        Initializable<T> ref = initializer.requestInjection(
+            injector, instance, source, injectionPoints);
         ConstantFactory<? extends T> factory = new ConstantFactory<T>(ref);
         InternalFactory<? extends T> scopedFactory = Scopes.scope(key, injector, factory, scope);
         putBinding(new InstanceBindingImpl<T>(injector, key, source, scopedFactory, injectionPoints,
@@ -135,7 +128,7 @@
       public Void visitProvider(Provider<? extends T> provider,
           Set<InjectionPoint> injectionPoints) {
         Initializable<Provider<? extends T>> initializable = initializer
-            .<Provider<? extends T>>requestInjection(provider, source, injectionPoints);
+            .<Provider<? extends T>>requestInjection(injector, provider, source, injectionPoints);
         InternalFactory<T> factory = new InternalFactoryToProviderAdapter<T>(initializable, source);
         InternalFactory<? extends T> scopedFactory = Scopes.scope(key, injector, factory, scope);
         putBinding(new ProviderInstanceBindingImpl<T>(injector, key, source, scopedFactory, scope,
@@ -144,8 +137,8 @@
       }
 
       public Void visitProviderKey(Key<? extends Provider<? extends T>> providerKey) {
-        final BoundProviderFactory<T> boundProviderFactory =
-            new BoundProviderFactory<T>(providerKey, source);
+        BoundProviderFactory<T> boundProviderFactory
+            = new BoundProviderFactory<T>(injector, providerKey, source);
         creationListeners.add(boundProviderFactory);
         InternalFactory<? extends T> scopedFactory = Scopes.scope(
             key, injector, (InternalFactory<? extends T>) boundProviderFactory, scope);
@@ -159,7 +152,7 @@
           errors.recursiveBinding();
         }
 
-        FactoryProxy<T> factory = new FactoryProxy<T>(key, targetKey, source);
+        FactoryProxy<T> factory = new FactoryProxy<T>(injector, key, targetKey, source);
         creationListeners.add(factory);
         InternalFactory<? extends T> scopedFactory = Scopes.scope(key, injector, factory, scope);
         putBinding(new LinkedBindingImpl<T>(
@@ -189,10 +182,10 @@
           return null;
         }
 
-        untargettedBindings.add(new Runnable() {
+        uninitializedBindings.add(new Runnable() {
           public void run() {
             try {
-              injector.initializeBinding(binding, errors.withSource(source));
+              binding.injector.initializeBinding(binding, errors.withSource(source));
             } catch (ErrorsException e) {
               errors.merge(e.getErrors());
             }
@@ -202,6 +195,13 @@
         return null;
       }
 
+      public Void visitExposed(PrivateEnvironment privateEnvironment) {
+        Factory<T> factory = new Factory<T>(key, privateEnvironment);
+        creationListeners.add(factory);
+        putBinding(new ExposedBindingImpl<T>(injector, source, factory));
+        return null;
+      }
+
       public Void visitConvertedConstant(T value) {
         throw new IllegalArgumentException("Cannot apply a non-module element");
       }
@@ -227,15 +227,15 @@
     return new InvalidBindingImpl<T>(injector, key, source);
   }
 
-  public void createUntargettedBindings() {
-    for (Runnable untargettedBinding : untargettedBindings) {
-      untargettedBinding.run();
+  public void initializeBindings() {
+    for (Runnable initializer : uninitializedBindings) {
+      initializer.run();
     }
   }
 
-  public void runCreationListeners(InjectorImpl injector) {
+  public void runCreationListeners(Map<PrivateEnvironment, InjectorImpl> privateInjectors) {
     for (CreationListener creationListener : creationListeners) {
-      creationListener.notify(injector, errors);
+      creationListener.notify(privateInjectors, errors);
     }
   }
 
@@ -248,22 +248,30 @@
       return;
     }
 
-    Binding<?> original = state.getExplicitBinding(key);
-
-    if (original != null) {
-      // the hard-coded class name is certainly lame, but it avoids an even lamer dependency...
-      boolean isOkayDuplicate = original instanceof ProviderInstanceBindingImpl
-          && "com.google.inject.privatemodules.PrivateModule$Expose"
-              .equals(original.acceptTargetVisitor(GET_BOUND_PROVIDER_CLASS_NAME));
-      if (!isOkayDuplicate) {
-        errors.bindingAlreadySet(key, original.getSource());
-        return;
-      }
+    Binding<?> original = injector.state.getExplicitBinding(key);
+    if (original != null && !isOkayDuplicate(original, binding)) {
+      errors.bindingAlreadySet(key, original.getSource());
+      return;
     }
 
     // prevent the parent from creating a JIT binding for this key
-    state.parent().blacklist(key);
-    state.putBinding(key, binding);
+    injector.state.parent().blacklist(key);
+    injector.state.putBinding(key, binding);
+  }
+
+  /**
+   * We tolerate duplicate bindings only if one exposes the other.
+   *
+   * @param original the binding in the parent injector (candidate for an exposing binding)
+   * @param binding the binding to check (candidate for the exposed binding)
+   */
+  private boolean isOkayDuplicate(Binding<?> original, BindingImpl<?> binding) {
+    if (original instanceof ExposedBindingImpl) {
+      ExposedBindingImpl exposed = (ExposedBindingImpl) original;
+      InjectorImpl exposedFrom = environmentToInjector.get(exposed.getPrivateEnvironment());
+      return (exposedFrom == binding.injector);
+    }
+    return false;
   }
 
   // It's unfortunate that we have to maintain a blacklist of specific
@@ -282,6 +290,6 @@
   // TODO(jessewilson): fix BuiltInModule, then add Stage
 
   interface CreationListener {
-    void notify(InjectorImpl injector, Errors errors);
+    void notify(Map<PrivateEnvironment, InjectorImpl> privateInjectors, Errors errors);
   }
 }
diff --git a/src/com/google/inject/BoundProviderFactory.java b/src/com/google/inject/BoundProviderFactory.java
index 705dd4e..8419653 100644
--- a/src/com/google/inject/BoundProviderFactory.java
+++ b/src/com/google/inject/BoundProviderFactory.java
@@ -20,24 +20,29 @@
 import com.google.inject.internal.Errors;
 import com.google.inject.internal.ErrorsException;
 import com.google.inject.spi.Dependency;
+import com.google.inject.spi.PrivateEnvironment;
+import java.util.Map;
 
 /**
  * Delegates to a custom factory which is also bound in the injector.
  */
 class BoundProviderFactory<T> implements InternalFactory<T>, CreationListener {
 
+  private final InjectorImpl injector;
   final Key<? extends Provider<? extends T>> providerKey;
   final Object source;
   private InternalFactory<? extends Provider<? extends T>> providerFactory;
 
   BoundProviderFactory(
+      InjectorImpl injector,
       Key<? extends Provider<? extends T>> providerKey,
       Object source) {
+    this.injector = injector;
     this.providerKey = providerKey;
     this.source = source;
   }
 
-  public void notify(final InjectorImpl injector, final Errors errors) {
+  public void notify(Map<PrivateEnvironment, InjectorImpl> privateInjectors, Errors errors) {
     try {
       providerFactory = injector.getInternalFactory(providerKey, errors.withSource(source));
     } catch (ErrorsException e) {
diff --git a/extensions/privatemodules/src/com/google/inject/privatemodules/Exposed.java b/src/com/google/inject/Exposed.java
similarity index 95%
rename from extensions/privatemodules/src/com/google/inject/privatemodules/Exposed.java
rename to src/com/google/inject/Exposed.java
index 264af8c..685c9c3 100644
--- a/extensions/privatemodules/src/com/google/inject/privatemodules/Exposed.java
+++ b/src/com/google/inject/Exposed.java
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package com.google.inject.privatemodules;
+package com.google.inject;
 
+import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 import java.lang.annotation.Target;
-import java.lang.annotation.Documented;
 
 /**
  * Acccompanies a {@literal @}{@link com.google.inject.Provides Provides} method annotation in a
diff --git a/src/com/google/inject/ExposedBindingImpl.java b/src/com/google/inject/ExposedBindingImpl.java
new file mode 100644
index 0000000..c5bfd8a
--- /dev/null
+++ b/src/com/google/inject/ExposedBindingImpl.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (C) 2008 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.inject;
+
+import com.google.inject.internal.Errors;
+import com.google.inject.internal.ErrorsException;
+import com.google.inject.internal.ToStringBuilder;
+import com.google.inject.spi.BindingTargetVisitor;
+import com.google.inject.spi.Dependency;
+import com.google.inject.spi.PrivateEnvironment;
+import java.util.Map;
+
+public class ExposedBindingImpl<T> extends BindingImpl<T> {
+
+  private Factory factory;
+
+  public ExposedBindingImpl(InjectorImpl injector, Object source, Factory<T> factory) {
+    super(injector, factory.key, source, factory, Scopes.NO_SCOPE, LoadStrategy.LAZY);
+    this.factory = factory;
+  }
+
+  public <V> V acceptTargetVisitor(BindingTargetVisitor<? super T, V> visitor) {
+    return visitor.visitExposed(factory.privateEnvironment);
+  }
+
+  public PrivateEnvironment getPrivateEnvironment() {
+    return factory.privateEnvironment;
+  }
+
+  @Override public String toString() {
+    return new ToStringBuilder(Binding.class)
+        .add("key", key)
+        .add("privateEnvironment", factory.privateEnvironment)
+        .add("scope", scope)
+        .add("source", source)
+        .toString();
+  }
+
+  static class Factory<T> implements InternalFactory<T>, BindingProcessor.CreationListener {
+    private final Key<T> key;
+    private final PrivateEnvironment privateEnvironment;
+    private BindingImpl<T> delegate;
+
+    Factory(Key<T> key, PrivateEnvironment privateEnvironment) {
+      this.key = key;
+      this.privateEnvironment = privateEnvironment;
+    }
+
+    public void notify(Map<PrivateEnvironment, InjectorImpl> privateInjectors, Errors errors) {
+      InjectorImpl privateInjector = privateInjectors.get(privateEnvironment);
+      BindingImpl<T> explicitBinding = privateInjector.state.getExplicitBinding(key);
+
+      if (explicitBinding.getInternalFactory() == this) {
+        errors.withSource(explicitBinding.getSource()).exposedButNotBound(key);
+        return;
+      }
+
+      this.delegate = explicitBinding;
+    }
+
+    public T get(Errors errors, InternalContext context, Dependency<?> dependency)
+        throws ErrorsException {
+      return delegate.getInternalFactory().get(errors, context, dependency);
+    }
+  }
+}
diff --git a/src/com/google/inject/FactoryProxy.java b/src/com/google/inject/FactoryProxy.java
index fef74ea..afcafb5 100644
--- a/src/com/google/inject/FactoryProxy.java
+++ b/src/com/google/inject/FactoryProxy.java
@@ -21,26 +21,29 @@
 import com.google.inject.internal.ErrorsException;
 import com.google.inject.internal.ToStringBuilder;
 import com.google.inject.spi.Dependency;
+import com.google.inject.spi.PrivateEnvironment;
+import java.util.Map;
 
 /**
- * A placeholder which enables us to swap in the real factory once the
- * container is created.
+ * A placeholder which enables us to swap in the real factory once the injector is created.
  */
 class FactoryProxy<T> implements InternalFactory<T>, BindingProcessor.CreationListener {
 
+  private final InjectorImpl injector;
   private final Key<T> key;
   private final Key<? extends T> targetKey;
   private final Object source;
 
   private InternalFactory<? extends T> targetFactory;
 
-  FactoryProxy(Key<T> key, Key<? extends T> targetKey, Object source) {
+  FactoryProxy(InjectorImpl injector, Key<T> key, Key<? extends T> targetKey, Object source) {
+    this.injector = injector;
     this.key = key;
     this.targetKey = targetKey;
     this.source = source;
   }
 
-  public void notify(final InjectorImpl injector, final Errors errors) {
+  public void notify(Map<PrivateEnvironment, InjectorImpl> privateInjectors, final Errors errors) {
     try {
       targetFactory = injector.getInternalFactory(targetKey, errors.withSource(source));
     } catch (ErrorsException e) {
diff --git a/src/com/google/inject/Initializer.java b/src/com/google/inject/Initializer.java
index 6427083..abac3f0 100644
--- a/src/com/google/inject/Initializer.java
+++ b/src/com/google/inject/Initializer.java
@@ -43,11 +43,6 @@
 
   /** Maps instances that need injection to a source that registered them */
   private final Map<Object, InjectableReference<?>> pendingInjection = Maps.newIdentityHashMap();
-  private final InjectorImpl injector;
-
-  Initializer(InjectorImpl injector) {
-    this.injector = injector;
-  }
 
   /**
    * Registers an instance for member injection when that step is performed.
@@ -56,7 +51,7 @@
    *      @Inject).
    * @param source the source location that this injection was requested
    */
-  public <T> Initializable<T> requestInjection(T instance, Object source,
+  public <T> Initializable<T> requestInjection(InjectorImpl injector, T instance, Object source,
       Set<InjectionPoint> injectionPoints) {
     checkNotNull(source);
 
@@ -65,7 +60,7 @@
       return Initializables.of(instance);
     }
 
-    InjectableReference<T> initializable = new InjectableReference<T>(instance, source);
+    InjectableReference<T> initializable = new InjectableReference<T>(injector, instance, source);
     pendingInjection.put(instance, initializable);
     return initializable;
   }
@@ -108,11 +103,13 @@
   }
 
   private class InjectableReference<T> implements Initializable<T> {
+    private final InjectorImpl injector;
     private final T instance;
     private final Object source;
     private List<SingleMemberInjector> injectors;
 
-    public InjectableReference(T instance, Object source) {
+    public InjectableReference(InjectorImpl injector, T instance, Object source) {
+      this.injector = injector;
       this.instance = checkNotNull(instance, "instance");
       this.source = checkNotNull(source, "source");
     }
diff --git a/src/com/google/inject/InjectionRequestProcessor.java b/src/com/google/inject/InjectionRequestProcessor.java
index b761b33..538e437 100644
--- a/src/com/google/inject/InjectionRequestProcessor.java
+++ b/src/com/google/inject/InjectionRequestProcessor.java
@@ -36,16 +36,15 @@
 class InjectionRequestProcessor extends AbstractProcessor {
 
   private final List<StaticInjection> staticInjections = Lists.newArrayList();
-  private final Initializer memberInjector;
+  private final Initializer initializer;
 
-  InjectionRequestProcessor(Errors errors,
-      Initializer memberInjector) {
+  InjectionRequestProcessor(Errors errors, Initializer initializer) {
     super(errors);
-    this.memberInjector = memberInjector;
+    this.initializer = initializer;
   }
 
   @Override public Boolean visitStaticInjectionRequest(StaticInjectionRequest command) {
-    staticInjections.add(new StaticInjection(command.getSource(), command.getType()));
+    staticInjections.add(new StaticInjection(injector, command.getSource(), command.getType()));
     return true;
   }
 
@@ -59,34 +58,36 @@
       injectionPoints = e.getPartialValue();
     }
 
-    memberInjector.requestInjection(command.getInstance(), command.getSource(), injectionPoints);
+    initializer.requestInjection(injector, command.getInstance(), command.getSource(), injectionPoints);
     return true;
   }
 
-  public void validate(InjectorImpl injector) {
+  public void validate() {
     for (StaticInjection staticInjection : staticInjections) {
-      staticInjection.validate(injector);
+      staticInjection.validate();
     }
   }
 
-  public void injectMembers(InjectorImpl injector) {
+  public void injectMembers() {
     for (StaticInjection staticInjection : staticInjections) {
-      staticInjection.injectMembers(injector);
+      staticInjection.injectMembers();
     }
   }
 
   /** A requested static injection. */
   private class StaticInjection {
+    final InjectorImpl injector;
     final Object source;
     final Class<?> type;
     ImmutableList<SingleMemberInjector> memberInjectors;
 
-    public StaticInjection(Object source, Class type) {
+    public StaticInjection(InjectorImpl injector, Object source, Class type) {
+      this.injector = injector;
       this.source = source;
       this.type = type;
     }
 
-    void validate(final InjectorImpl injector) {
+    void validate() {
       Errors errorsForMember = errors.withSource(source);
       Set<InjectionPoint> injectionPoints;
       try {
@@ -98,7 +99,7 @@
       memberInjectors = injector.getInjectors(injectionPoints, errorsForMember);
     }
 
-    void injectMembers(InjectorImpl injector) {
+    void injectMembers() {
       try {
         injector.callInContext(new ContextualCallable<Void>() {
           public Void call(InternalContext context) {
diff --git a/src/com/google/inject/InjectorBuilder.java b/src/com/google/inject/InjectorBuilder.java
index 8932423..916200c 100644
--- a/src/com/google/inject/InjectorBuilder.java
+++ b/src/com/google/inject/InjectorBuilder.java
@@ -16,27 +16,25 @@
 
 package com.google.inject;
 
-import static com.google.common.base.Preconditions.checkNotNull;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import static com.google.inject.Scopes.SINGLETON;
+import com.google.common.collect.Maps;
 import com.google.inject.internal.Errors;
 import com.google.inject.internal.ErrorsException;
-import com.google.inject.internal.SourceProvider;
 import com.google.inject.internal.Stopwatch;
 import com.google.inject.spi.Dependency;
-import com.google.inject.spi.Element;
-import com.google.inject.spi.Elements;
-import com.google.inject.spi.InjectionPoint;
+import com.google.inject.spi.PrivateEnvironment;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.logging.Logger;
 
 /**
- * Builds a dependency injection {@link Injector}. Injector construction happens in two phases.
+ * Builds a tree of injectors. This is a primary injector, plus child injectors needed for each
+ * {@link Binder#newPrivateBinder() private environment}. The primary injector is not necessarily a
+ * top-level injector.
+ *
+ * <p>Injector construction happens in two phases.
  * <ol>
  *   <li>Static building. In this phase, we interpret commands, create bindings, and inspect 
  *     dependencies. During this phase, we hold a lock to ensure consistency with parent injectors.
@@ -53,125 +51,111 @@
 class InjectorBuilder {
 
   private final Stopwatch stopwatch = new Stopwatch();
+  private final Errors errors = new Errors();
 
-  private InjectorImpl parent = null;
   private Stage stage;
-  private final List<Module> modules = Lists.newLinkedList();
 
-  private InjectorImpl injector;
-  private Errors errors = new Errors();
+  private final Initializer initializer = new Initializer();
+  private final BindingProcessor bindingProcesor;
+  private final InjectionRequestProcessor injectionRequestProcessor;
+  private final Map<PrivateEnvironment, InjectorImpl> environmentToInjector = Maps.newHashMap();
 
-  private final List<Element> elements = Lists.newArrayList();
+  private final InjectorShell.Builder shellBuilder = new InjectorShell.Builder();
+  private List<InjectorShell> shells;
 
-  private BindingProcessor bindCommandProcesor;
-  private InjectionRequestProcessor injectionCommandProcessor;
+  InjectorBuilder() {
+    injectionRequestProcessor = new InjectionRequestProcessor(errors, initializer);
+    bindingProcesor = new BindingProcessor(errors, initializer, environmentToInjector);
+  }
 
   /**
-   * @param stage we're running in. If the stage is {@link Stage#PRODUCTION}, we will eagerly load
-   * singletons.
+   * Sets the stage for the created injector. If the stage is {@link Stage#PRODUCTION}, this class
+   * will eagerly load singletons.
    */
   InjectorBuilder stage(Stage stage) {
+    shellBuilder.stage(stage);
     this.stage = stage;
     return this;
   }
 
+  /**
+   * Sets the parent of the injector to-be-constructed. As a side effect, this sets this injector's
+   * stage to the stage of {@code parent}.
+   */
   InjectorBuilder parentInjector(InjectorImpl parent) {
-    this.parent = parent;
-    return this;
+    shellBuilder.parent(parent);
+    return stage(parent.getInstance(Stage.class));
   }
 
   InjectorBuilder addModules(Iterable<? extends Module> modules) {
-    for (Module module : modules) {
-      this.modules.add(module);
-    }
+    shellBuilder.addModules(modules);
     return this;
   }
 
   Injector build() {
-    if (injector != null) {
+    if (shellBuilder == null) {
       throw new AssertionError("Already built, builders are not reusable.");
     }
 
-    injector = new InjectorImpl(parent);
+    // Synchronize while we're building up the bindings and other injector state. This ensures that
+    // the JIT bindings in the parent injector don't change while we're being built
+    synchronized (shellBuilder.lock()) {
+      shells = shellBuilder.build(
+          initializer, bindingProcesor, environmentToInjector, stopwatch, errors);
+      stopwatch.resetAndLog("Injector construction");
 
-    buildStatically();
+      initializeStatically();
+    }
 
     // If we're in the tool stage, stop here. Don't eagerly inject or load anything.
     if (stage == Stage.TOOL) {
-      return new ToolStageInjector(injector);
+      return new ToolStageInjector(primaryInjector());
     }
 
     injectDynamically();
 
-    return injector;
+    return primaryInjector();
+  }
+
+  /** Initialize and validate everything. */
+  private void initializeStatically() {
+    bindingProcesor.initializeBindings();
+    stopwatch.resetAndLog("Binding initialization");
+
+    for (InjectorShell shell : shells) {
+      shell.getInjector().index();
+    }
+    stopwatch.resetAndLog("Binding indexing");
+
+    injectionRequestProcessor.process(shells);
+    stopwatch.resetAndLog("Collecting injection requests");
+
+    bindingProcesor.runCreationListeners(environmentToInjector);
+    stopwatch.resetAndLog("Binding validation");
+
+    injectionRequestProcessor.validate();
+    stopwatch.resetAndLog("Static validation");
+
+    initializer.validateOustandingInjections(errors);
+    stopwatch.resetAndLog("Instance member validation");
+
+    new ProviderLookupProcessor(errors).process(shells);
+    stopwatch.resetAndLog("Provider verification");
+
+    for (InjectorShell shell : shells) {
+      if (!shell.getElements().isEmpty()) {
+        throw new AssertionError("Failed to execute " + shell.getElements());
+      }
+    }
+
+    errors.throwCreationExceptionIfErrorsExist();
   }
 
   /**
-   * Synchronize while we're building up the bindings and other injector state. This ensures that
-   * the JIT bindings in the parent injector don't change while we're being built
+   * Returns the injector being constructed. This is not necessarily the root injector.
    */
-  void buildStatically() {
-    synchronized (injector.state.lock()) {
-      // bind Stage and Singleton if this is a top-level injector
-      if (parent == null) {
-        modules.add(0, new RootModule(stage));
-      }
-
-      elements.addAll(Elements.getElements(stage, modules));
-      stopwatch.resetAndLog("Module execution");
-
-      new MessageProcessor(errors).processCommands(elements);
-
-      InterceptorBindingProcessor interceptorCommandProcessor
-          = new InterceptorBindingProcessor(errors, injector.state);
-      interceptorCommandProcessor.processCommands(elements);
-      injector.constructionProxyFactory = interceptorCommandProcessor.createProxyFactory();
-      stopwatch.resetAndLog("Interceptors creation");
-
-      new ScopeBindingProcessor(errors, injector.state).processCommands(elements);
-      stopwatch.resetAndLog("Scopes creation");
-
-      new TypeConverterBindingProcessor(errors, injector.state).processCommands(elements);
-      stopwatch.resetAndLog("Converters creation");
-
-      bindInjector();
-      bindLogger();
-      bindCommandProcesor = new BindingProcessor(errors,
-          injector, injector.state, injector.initializer);
-      bindCommandProcesor.processCommands(elements);
-      bindCommandProcesor.createUntargettedBindings();
-      stopwatch.resetAndLog("Binding creation");
-
-      injector.index();
-      stopwatch.resetAndLog("Binding indexing");
-
-      injectionCommandProcessor = new InjectionRequestProcessor(errors, injector.initializer);
-      injectionCommandProcessor.processCommands(elements);
-      stopwatch.resetAndLog("Static injection");
-
-      validateStatically();
-
-      if (!elements.isEmpty()) {
-        throw new AssertionError("Failed to execute " + elements);
-      }
-    }
-  }
-
-  /** Validate everything that we can validate now that the injector is ready for use. */
-  private void validateStatically() {
-    bindCommandProcesor.runCreationListeners(injector);
-    stopwatch.resetAndLog("Validation");
-
-    injectionCommandProcessor.validate(injector);
-    stopwatch.resetAndLog("Static validation");
-
-    injector.initializer.validateOustandingInjections(errors);
-    stopwatch.resetAndLog("Instance member validation");
-
-    new ProviderLookupProcessor(errors, injector).processCommands(elements);
-    stopwatch.resetAndLog("Provider verification");
-
-    errors.throwCreationExceptionIfErrorsExist();
+  private Injector primaryInjector() {
+    return shells.get(0).getInjector();
   }
 
   /**
@@ -180,27 +164,31 @@
    * code build a just-in-time binding from another thread.
    */
   private void injectDynamically() {
-    injectionCommandProcessor.injectMembers(injector);
+    injectionRequestProcessor.injectMembers();
     stopwatch.resetAndLog("Static member injection");
 
-    injector.initializer.injectAll(errors);
+    initializer.injectAll(errors);
     stopwatch.resetAndLog("Instance injection");
     errors.throwCreationExceptionIfErrorsExist();
 
-    loadEagerSingletons();
-    stopwatch.resetAndLog("Preloading");
+    for (InjectorShell shell : shells) {
+      loadEagerSingletons(shell.getInjector(), stage, errors);
+    }
+    stopwatch.resetAndLog("Preloading singletons");
     errors.throwCreationExceptionIfErrorsExist();
   }
 
-  public void loadEagerSingletons() {
-    // load eager singletons, or all singletons if we're in Stage.PRODUCTION.
-    // Bindings discovered while we're binding these singletons are not be eager.
+  /**
+   * Loads eager singletons, or all singletons if we're in Stage.PRODUCTION. Bindings discovered
+   * while we're binding these singletons are not be eager.
+   */
+  public void loadEagerSingletons(InjectorImpl injector, Stage stage, final Errors errors) {
     @SuppressWarnings("unchecked") // casting Collection<Binding> to Collection<BindingImpl> is safe
     Set<BindingImpl<?>> candidateBindings = ImmutableSet.copyOf(Iterables.concat(
         (Collection) injector.state.getExplicitBindingsThisLevel().values(),
         injector.jitBindings.values()));
     for (final BindingImpl<?> binding : candidateBindings) {
-      if ((stage == Stage.PRODUCTION && binding.getScope() == SINGLETON)
+      if ((stage == Stage.PRODUCTION && binding.getScope() == com.google.inject.Scopes.SINGLETON)
           || binding.getLoadStrategy() == LoadStrategy.EAGER) {
         try {
           injector.callInContext(new ContextualCallable<Void>() {
@@ -226,84 +214,6 @@
     }
   }
 
-  private static class RootModule implements Module {
-    final Stage stage;
-
-    private RootModule(Stage stage) {
-      this.stage = checkNotNull(stage, "stage");
-    }
-
-    public void configure(Binder binder) {
-      binder = binder.withSource(SourceProvider.UNKNOWN_SOURCE);
-      binder.bind(Stage.class).toInstance(stage);
-      binder.bindScope(Singleton.class, SINGLETON);
-    }
-  }
-
-  /**
-   * The Injector is a special case because we allow both parent and child injectors to both have
-   * a binding for that key.
-   */
-  private void bindInjector() {
-    Key<Injector> key = Key.get(Injector.class);
-    InjectorFactory injectorFactory = new InjectorFactory(injector);
-    injector.state.putBinding(key,
-        new ProviderInstanceBindingImpl<Injector>(injector, key, SourceProvider.UNKNOWN_SOURCE,
-            injectorFactory, Scopes.NO_SCOPE, injectorFactory, LoadStrategy.LAZY,
-            ImmutableSet.<InjectionPoint>of()));
-  }
-
-  static class InjectorFactory implements  InternalFactory<Injector>, Provider<Injector> {
-    private final Injector injector;
-
-    private InjectorFactory(Injector injector) {
-      this.injector = injector;
-    }
-
-    public Injector get(Errors errors, InternalContext context, Dependency<?> dependency)
-        throws ErrorsException {
-      return injector;
-    }
-
-    public Injector get() {
-      return injector;
-    }
-
-    public String toString() {
-      return "Provider<Injector>";
-    }
-  }
-
-  /**
-   * The Logger is a special case because it knows the injection point of the injected member. It's
-   * the only binding that does this.
-   */
-  private void bindLogger() {
-    Key<Logger> key = Key.get(Logger.class);
-    LoggerFactory loggerFactory = new LoggerFactory();
-    injector.state.putBinding(key,
-        new ProviderInstanceBindingImpl<Logger>(injector, key,
-            SourceProvider.UNKNOWN_SOURCE, loggerFactory, Scopes.NO_SCOPE,
-            loggerFactory, LoadStrategy.LAZY, ImmutableSet.<InjectionPoint>of()));
-  }
-
-  static class LoggerFactory implements InternalFactory<Logger>, Provider<Logger> {
-    public Logger get(Errors errors, InternalContext context, Dependency<?> dependency) {
-      InjectionPoint injectionPoint = dependency.getInjectionPoint();
-      return injectionPoint == null
-          ? Logger.getAnonymousLogger()
-          : Logger.getLogger(injectionPoint.getMember().getDeclaringClass().getName());
-    }
-
-    public Logger get() {
-      return Logger.getAnonymousLogger();
-    }
-
-    public String toString() {
-      return "Provider<Logger>";
-    }
-  }
-
   /** {@link Injector} exposed to users in {@link Stage#TOOL}. */
   static class ToolStageInjector implements Injector {
     private final Injector delegateInjector;
diff --git a/src/com/google/inject/InjectorImpl.java b/src/com/google/inject/InjectorImpl.java
index f58cb90..b3918cf 100644
--- a/src/com/google/inject/InjectorImpl.java
+++ b/src/com/google/inject/InjectorImpl.java
@@ -61,12 +61,13 @@
   final State state;
   final InjectorImpl parent;
   final BindingsMultimap bindingsMultimap = new BindingsMultimap();
-  final Initializer initializer = new Initializer(this);
+  final Initializer initializer;
   ConstructionProxyFactory constructionProxyFactory;
 
-  InjectorImpl(@Nullable InjectorImpl parent) {
+  InjectorImpl(@Nullable InjectorImpl parent, State state, Initializer initializer) {
     this.parent = parent;
-    this.state = new InheritingState(parent != null ? parent.state : State.NONE);
+    this.state = state;
+    this.initializer = initializer;
 
     if (parent != null) {
       localContext = parent.localContext;
@@ -135,7 +136,6 @@
   public Injector createChildInjector(Iterable<? extends Module> modules) {
     return new InjectorBuilder()
         .parentInjector(this)
-        .stage(getInstance(Stage.class))
         .addModules(modules)
         .build();
   }
diff --git a/src/com/google/inject/InjectorShell.java b/src/com/google/inject/InjectorShell.java
new file mode 100644
index 0000000..844fe7c
--- /dev/null
+++ b/src/com/google/inject/InjectorShell.java
@@ -0,0 +1,252 @@
+/**
+ * Copyright (C) 2008 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.inject;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import static com.google.inject.Scopes.SINGLETON;
+import com.google.inject.internal.Errors;
+import com.google.inject.internal.ErrorsException;
+import com.google.inject.internal.SourceProvider;
+import com.google.inject.internal.Stopwatch;
+import com.google.inject.spi.Dependency;
+import com.google.inject.spi.Element;
+import com.google.inject.spi.Elements;
+import com.google.inject.spi.InjectionPoint;
+import com.google.inject.spi.PrivateEnvironment;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+/**
+ * A partially-initialized injector. See {@link InjectorBuilder}, which uses this to build a tree
+ * of injectors in batch.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+class InjectorShell {
+
+  private final List<Element> elements;
+  private final InjectorImpl injector;
+  private final PrivateEnvironment privateEnvironment;
+
+  private InjectorShell(Builder builder, List<Element> elements, InjectorImpl injector) {
+    this.privateEnvironment = builder.privateEnvironment;
+    this.elements = elements;
+    this.injector = injector;
+  }
+
+  PrivateEnvironment getPrivateEnvironment() {
+    return privateEnvironment;
+  }
+
+  InjectorImpl getInjector() {
+    return injector;
+  }
+
+  List<Element> getElements() {
+    return elements;
+  }
+
+  static class Builder {
+    private final List<Element> elements = Lists.newArrayList();
+    private final List<Module> modules = Lists.newArrayList();
+
+    /** lazily constructed */
+    private State state;
+
+    private InjectorImpl parent;
+    private Stage stage;
+
+    /** null unless this exists in a {@link Binder#newPrivateBinder private environment} */
+    private PrivateEnvironment privateEnvironment;
+
+    Builder parent(InjectorImpl parent) {
+      this.parent = parent;
+      this.state = new InheritingState(parent.state);
+      return this;
+    }
+
+    Builder stage(Stage stage) {
+      this.stage = stage;
+      return this;
+    }
+
+    Builder privateEnvironment(PrivateEnvironment privateEnvironment) {
+      this.privateEnvironment = privateEnvironment;
+      this.elements.addAll(privateEnvironment.getElements());
+      return this;
+    }
+
+    void addModules(Iterable<? extends Module> modules) {
+      for (Module module : modules) {
+        this.modules.add(module);
+      }
+    }
+
+    /** Synchronize on this before calling {@link #build}. */
+    Object lock() {
+      return getState().lock();
+    }
+
+    /**
+     * Creates and returns the injector shells for the current modules. Multiple shells will be
+     * returned if any modules contain {@link Binder#newPrivateBinder private environments}. The
+     * primary injector will be first in the returned list.
+     */
+    List<InjectorShell> build(Initializer initializer, BindingProcessor bindingProcessor,
+        Map<PrivateEnvironment, InjectorImpl> environmentToInjector,
+        Stopwatch stopwatch, Errors errors) {
+      checkState(stage != null, "Stage not initialized");
+      checkState(privateEnvironment == null || parent != null, "PrivateEnvironment with no parent");
+      checkState(state != null, "no state. Did you remember to lock() ?");
+
+      InjectorImpl injector = new InjectorImpl(parent, state, initializer);
+      if (privateEnvironment != null) {
+        environmentToInjector.put(privateEnvironment, injector);
+      }
+
+      // bind Stage and Singleton if this is a top-level injector
+      if (parent == null) {
+        modules.add(0, new RootModule(stage));
+        new TypeConverterBindingProcessor(errors).prepareBuiltInConverters(injector);
+      }
+
+      elements.addAll(Elements.getElements(stage, modules));
+      stopwatch.resetAndLog("Module execution");
+
+      new MessageProcessor(errors).process(injector, elements);
+
+      InterceptorBindingProcessor interceptors = new InterceptorBindingProcessor(errors);
+      interceptors.process(injector, elements);
+      interceptors.setupProxyFactory(injector);
+      stopwatch.resetAndLog("Interceptors creation");
+
+      new ScopeBindingProcessor(errors).process(injector, elements);
+      stopwatch.resetAndLog("Scopes creation");
+
+      new TypeConverterBindingProcessor(errors).process(injector, elements);
+      stopwatch.resetAndLog("Converters creation");
+
+      bindInjector(injector);
+      bindLogger(injector);
+      bindingProcessor.process(injector, elements);
+      stopwatch.resetAndLog("Binding creation");
+
+      List<InjectorShell> injectorShells = Lists.newArrayList();
+      injectorShells.add(new InjectorShell(this, elements, injector));
+
+      // recursively build child shells
+      PrivateEnvironmentProcessor processor = new PrivateEnvironmentProcessor(errors, stage);
+      processor.process(injector, elements);
+      for (Builder builder : processor.getInjectorShellBuilders()) {
+        injectorShells.addAll(
+            builder.build(initializer, bindingProcessor, environmentToInjector, stopwatch, errors));
+      }
+      stopwatch.resetAndLog("Private environment creation");
+
+      return injectorShells;
+    }
+
+    private State getState() {
+      if (state == null) {
+        state = new InheritingState(State.NONE);
+      }
+      return state;
+    }
+  }
+
+  /**
+   * The Injector is a special case because we allow both parent and child injectors to both have
+   * a binding for that key.
+   */
+  private static void bindInjector(InjectorImpl injector) {
+    Key<Injector> key = Key.get(Injector.class);
+    InjectorFactory injectorFactory = new InjectorFactory(injector);
+    injector.state.putBinding(key,
+        new ProviderInstanceBindingImpl<Injector>(injector, key, SourceProvider.UNKNOWN_SOURCE,
+            injectorFactory, Scopes.NO_SCOPE, injectorFactory, LoadStrategy.LAZY,
+            ImmutableSet.<InjectionPoint>of()));
+  }
+
+  private static class InjectorFactory implements InternalFactory<Injector>, Provider<Injector> {
+    private final Injector injector;
+
+    private InjectorFactory(Injector injector) {
+      this.injector = injector;
+    }
+
+    public Injector get(Errors errors, InternalContext context, Dependency<?> dependency)
+        throws ErrorsException {
+      return injector;
+    }
+
+    public Injector get() {
+      return injector;
+    }
+
+    public String toString() {
+      return "Provider<Injector>";
+    }
+  }
+
+  /**
+   * The Logger is a special case because it knows the injection point of the injected member. It's
+   * the only binding that does this.
+   */
+  private static void bindLogger(InjectorImpl injector) {
+    Key<Logger> key = Key.get(Logger.class);
+    LoggerFactory loggerFactory = new LoggerFactory();
+    injector.state.putBinding(key,
+        new ProviderInstanceBindingImpl<Logger>(injector, key,
+            SourceProvider.UNKNOWN_SOURCE, loggerFactory, Scopes.NO_SCOPE,
+            loggerFactory, LoadStrategy.LAZY, ImmutableSet.<InjectionPoint>of()));
+  }
+
+  private static class LoggerFactory implements InternalFactory<Logger>, Provider<Logger> {
+    public Logger get(Errors errors, InternalContext context, Dependency<?> dependency) {
+      InjectionPoint injectionPoint = dependency.getInjectionPoint();
+      return injectionPoint == null
+          ? Logger.getAnonymousLogger()
+          : Logger.getLogger(injectionPoint.getMember().getDeclaringClass().getName());
+    }
+
+    public Logger get() {
+      return Logger.getAnonymousLogger();
+    }
+
+    public String toString() {
+      return "Provider<Logger>";
+    }
+  }
+
+  private static class RootModule implements Module {
+    final Stage stage;
+
+    private RootModule(Stage stage) {
+      this.stage = checkNotNull(stage, "stage");
+    }
+
+    public void configure(Binder binder) {
+      binder = binder.withSource(SourceProvider.UNKNOWN_SOURCE);
+      binder.bind(Stage.class).toInstance(stage);
+      binder.bindScope(Singleton.class, SINGLETON);
+    }
+  }
+}
diff --git a/src/com/google/inject/InterceptorBindingProcessor.java b/src/com/google/inject/InterceptorBindingProcessor.java
index ef1c2f9..3967355 100644
--- a/src/com/google/inject/InterceptorBindingProcessor.java
+++ b/src/com/google/inject/InterceptorBindingProcessor.java
@@ -27,20 +27,17 @@
  */
 class InterceptorBindingProcessor extends AbstractProcessor {
 
-  private final State state;
-
-  InterceptorBindingProcessor(Errors errors, State state) {
+  InterceptorBindingProcessor(Errors errors) {
     super(errors);
-    this.state = state;
   }
 
   @Override public Boolean visitInterceptorBinding(InterceptorBinding command) {
-    state.addMethodAspect(new MethodAspect(
+    injector.state.addMethodAspect(new MethodAspect(
         command.getClassMatcher(), command.getMethodMatcher(), command.getInterceptors()));
     return true;
   }
 
-  ProxyFactory createProxyFactory() {
-    return new ProxyFactory(state.getMethodAspects());
+  void setupProxyFactory(InjectorImpl injector) {
+    injector.constructionProxyFactory = new ProxyFactory(injector.state.getMethodAspects());
   }
 }
diff --git a/src/com/google/inject/PrivateEnvironmentProcessor.java b/src/com/google/inject/PrivateEnvironmentProcessor.java
new file mode 100644
index 0000000..cfccab7
--- /dev/null
+++ b/src/com/google/inject/PrivateEnvironmentProcessor.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright (C) 2008 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.inject;
+
+import com.google.common.collect.Lists;
+import com.google.inject.internal.Errors;
+import com.google.inject.spi.PrivateEnvironment;
+import java.util.List;
+
+/**
+ * Handles {@link Binder#newPrivateBinder()} elements.
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public class PrivateEnvironmentProcessor extends AbstractProcessor {
+
+  private final Stage stage;
+  private final List<InjectorShell.Builder> injectorShellBuilders = Lists.newArrayList();
+
+  PrivateEnvironmentProcessor(Errors errors, Stage stage) {
+    super(errors);
+    this.stage = stage;
+  }
+
+  @Override public Boolean visitPrivateEnvironment(PrivateEnvironment privateEnvironment) {
+    InjectorShell.Builder builder = new InjectorShell.Builder()
+        .parent(injector)
+        .stage(stage)
+        .privateEnvironment(privateEnvironment);
+    injectorShellBuilders.add(builder);
+    return true;
+  }
+
+  public List<InjectorShell.Builder> getInjectorShellBuilders() {
+    return injectorShellBuilders;
+  }
+}
diff --git a/src/com/google/inject/PrivateModule.java b/src/com/google/inject/PrivateModule.java
new file mode 100644
index 0000000..277acba
--- /dev/null
+++ b/src/com/google/inject/PrivateModule.java
@@ -0,0 +1,213 @@
+/**
+ * Copyright (C) 2008 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.inject;
+
+import static com.google.common.base.Preconditions.checkState;
+import com.google.inject.binder.AnnotatedBindingBuilder;
+import com.google.inject.binder.AnnotatedConstantBindingBuilder;
+import com.google.inject.binder.AnnotatedElementBuilder;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.binder.PrivateBinder;
+import com.google.inject.matcher.Matcher;
+import com.google.inject.spi.Message;
+import com.google.inject.spi.TypeConverter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import org.aopalliance.intercept.MethodInterceptor;
+
+/**
+ * A module whose configuration information is hidden from its environment by default. Only bindings
+ * that are explicitly exposed will be available to other modules and to the users of the injector.
+ * This module may expose the bindings it creates and the bindings of the modules it installs.
+ *
+ * <p>A private module can be nested within a regular module or within another private module using
+ * {@link Binder#install install()}.  Its bindings live in a new environment that inherits bindings,
+ * type converters, scopes, and interceptors from the surrounding ("parent") environment.  When you
+ * nest multiple private modules, the result is a tree of environments where the injector's
+ * environment is the root.
+ *
+ * <p>Guice EDSL bindings can be exposed with {@link #expose(Class) expose()}. {@literal @}{@link
+ * com.google.inject.Provides Provides} bindings can be exposed with the {@literal @}{@link
+ * Exposed} annotation:
+ *
+ * <pre>
+ * public class FooBarBazModule extends PrivateModule {
+ *   protected void configurePrivateBindings() {
+ *     bind(Foo.class).to(RealFoo.class);
+ *     expose(Foo.class);
+ *
+ *     install(new TransactionalBarModule());
+ *     expose(Bar.class).annotatedWith(Transactional.class);
+ *
+ *     bind(SomeImplementationDetail.class);
+ *     install(new MoreImplementationDetailsModule());
+ *   }
+ *
+ *   {@literal @}Provides {@literal @}Exposed
+ *   public Baz provideBaz() {
+ *     return new SuperBaz();
+ *   }
+ * }
+ * </pre>
+ *
+ * <p>Private modules are implemented using {@link Injector#createChildInjector(Module[]) parent
+ * injectors}. When it can satisfy their dependencies, just-in-time bindings will be created in the
+ * root environment. Such bindings are shared among all environments in the tree.
+ * 
+ * <p>The scope of a binding is constrained to its environment. A singleton bound in a private
+ * module will be unique to its environment. But a binding for the same type in a different private
+ * module will yield a different instance.
+ *
+ * <p>A shared binding that injects the {@code Injector} gets the root injector, which only has
+ * access to bindings in the root environment. An explicit binding that injects the {@code Injector}
+ * gets access to all bindings in the child environment.
+ *
+ * <p>To promote a just-in-time binding to an explicit binding, bind it:
+ * <pre>
+ *   bind(FooImpl.class);
+ * </pre>
+ *
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public abstract class PrivateModule implements Module {
+
+  /** Like abstract module, the binder of the current private module */
+  private PrivateBinder binder;
+
+  public final synchronized void configure(Binder binder) {
+    checkState(this.binder == null, "Re-entry is not allowed.");
+
+    this.binder = (PrivateBinder) binder.skipSources(PrivateModule.class);
+    try {
+      configurePrivateBindings();
+    } finally {
+      this.binder = null;
+    }
+  }
+
+  /**
+   * Creates bindings and other configurations private to this module. Use {@link #expose(Class)
+   * expose()} to make the bindings in this module available externally.
+   */
+  protected abstract void configurePrivateBindings();
+
+  /** Makes the binding for {@code key} available to other modules and the injector. */
+  protected final <T> void expose(Key<T> key) {
+    binder.expose(key);
+  }
+
+  /**
+   * Makes a binding for {@code type} available to other modules and the injector. Use {@link
+   * AnnotatedElementBuilder#annotatedWith(Class) annotatedWith()} to expose {@code type} with a
+   * binding annotation.
+   */
+  protected final AnnotatedElementBuilder expose(Class<?> type) {
+    return binder.expose(type);
+  }
+
+  /**
+   * Makes a binding for {@code type} available to other modules and the injector. Use {@link
+   * AnnotatedElementBuilder#annotatedWith(Class) annotatedWith()} to expose {@code type} with a
+   * binding annotation.
+   */
+  protected final AnnotatedElementBuilder expose(TypeLiteral<?> type) {
+    return binder.expose(type);
+  }
+
+  // prevent classes migrated from AbstractModule from implementing the wrong method.
+  protected final void configure() {}
+
+  // everything below is copied from AbstractModule
+
+  protected final Binder binder() {
+    return binder;
+  }
+
+  protected final void bindScope(Class<? extends Annotation> scopeAnnotation, Scope scope) {
+    binder.bindScope(scopeAnnotation, scope);
+  }
+
+  protected final <T> LinkedBindingBuilder<T> bind(Key<T> key) {
+    return binder.bind(key);
+  }
+
+  protected final <T> AnnotatedBindingBuilder<T> bind(TypeLiteral<T> typeLiteral) {
+    return binder.bind(typeLiteral);
+  }
+
+  protected final <T> AnnotatedBindingBuilder<T> bind(Class<T> clazz) {
+    return binder.bind(clazz);
+  }
+
+  protected final AnnotatedConstantBindingBuilder bindConstant() {
+    return binder.bindConstant();
+  }
+
+  protected final void install(Module module) {
+    binder.install(module);
+  }
+
+  protected final void addError(String message, Object... arguments) {
+    binder.addError(message, arguments);
+  }
+
+  protected final void addError(Throwable t) {
+    binder.addError(t);
+  }
+
+  protected final void addError(Message message) {
+    binder.addError(message);
+  }
+
+  protected final void requestInjection(Object... objects) {
+    binder.requestInjection(objects);
+  }
+
+  protected final void requestStaticInjection(Class<?>... types) {
+    binder.requestStaticInjection(types);
+  }
+
+  protected final void bindInterceptor(Matcher<? super Class<?>> classMatcher,
+      Matcher<? super Method> methodMatcher, MethodInterceptor... interceptors) {
+    binder.bindInterceptor(classMatcher, methodMatcher, interceptors);
+  }
+
+  protected final void requireBinding(Key<?> key) {
+    binder.getProvider(key);
+  }
+
+  protected final void requireBinding(Class<?> type) {
+    binder.getProvider(type);
+  }
+
+  protected final <T> Provider<T> getProvider(Key<T> key) {
+    return binder.getProvider(key);
+  }
+
+  protected final <T> Provider<T> getProvider(Class<T> type) {
+    return binder.getProvider(type);
+  }
+
+  protected final void convertToTypes(Matcher<? super TypeLiteral<?>> typeMatcher,
+      TypeConverter converter) {
+    binder.convertToTypes(typeMatcher, converter);
+  }
+
+  protected final Stage currentStage() {
+    return binder.currentStage();
+  }
+}
diff --git a/src/com/google/inject/ProviderLookupProcessor.java b/src/com/google/inject/ProviderLookupProcessor.java
index f50d3ed..8c06228 100644
--- a/src/com/google/inject/ProviderLookupProcessor.java
+++ b/src/com/google/inject/ProviderLookupProcessor.java
@@ -28,11 +28,8 @@
  */
 class ProviderLookupProcessor extends AbstractProcessor {
 
-  private final InjectorImpl injector;
-
-  ProviderLookupProcessor(Errors errors, InjectorImpl injector) {
+  ProviderLookupProcessor(Errors errors) {
     super(errors);
-    this.injector = injector;
   }
 
   @Override public <T> Boolean visitProviderLookup(ProviderLookup<T> command) {
diff --git a/src/com/google/inject/ScopeBindingProcessor.java b/src/com/google/inject/ScopeBindingProcessor.java
index 5936fe9..5c6fb39 100644
--- a/src/com/google/inject/ScopeBindingProcessor.java
+++ b/src/com/google/inject/ScopeBindingProcessor.java
@@ -30,12 +30,8 @@
  */
 class ScopeBindingProcessor extends AbstractProcessor {
 
-  private final State state;
-
-  ScopeBindingProcessor(Errors errors,
-      State state) {
+  ScopeBindingProcessor(Errors errors) {
     super(errors);
-    this.state = state;
   }
 
   @Override public Boolean visitScopeBinding(ScopeBinding command) {
@@ -53,11 +49,11 @@
       // Go ahead and bind anyway so we don't get collateral errors.
     }
 
-    Scope existing = state.getScope(checkNotNull(annotationType, "annotation type"));
+    Scope existing = injector.state.getScope(checkNotNull(annotationType, "annotation type"));
     if (existing != null) {
       errors.duplicateScopes(existing, annotationType, scope);
     } else {
-      state.putAnnotation(annotationType, checkNotNull(scope, "scope"));
+      injector.state.putAnnotation(annotationType, checkNotNull(scope, "scope"));
     }
 
     return true;
diff --git a/src/com/google/inject/TypeConverterBindingProcessor.java b/src/com/google/inject/TypeConverterBindingProcessor.java
index b9d1fd3..b2e5885 100644
--- a/src/com/google/inject/TypeConverterBindingProcessor.java
+++ b/src/com/google/inject/TypeConverterBindingProcessor.java
@@ -38,76 +38,79 @@
  */
 class TypeConverterBindingProcessor extends AbstractProcessor {
 
-  private final State state;
-
-  TypeConverterBindingProcessor(Errors errors, State state) {
+  TypeConverterBindingProcessor(Errors errors) {
     super(errors);
-    this.state = state;
-
-    // Configure type converters.
-    convertToPrimitiveType(int.class, Integer.class);
-    convertToPrimitiveType(long.class, Long.class);
-    convertToPrimitiveType(boolean.class, Boolean.class);
-    convertToPrimitiveType(byte.class, Byte.class);
-    convertToPrimitiveType(short.class, Short.class);
-    convertToPrimitiveType(float.class, Float.class);
-    convertToPrimitiveType(double.class, Double.class);
-
-    convertToClass(Character.class, new TypeConverter() {
-      public Object convert(String value, TypeLiteral<?> toType) {
-        value = value.trim();
-        if (value.length() != 1) {
-          throw new RuntimeException("Length != 1.");
-        }
-        return value.charAt(0);
-      }
-
-      @Override public String toString() {
-        return "TypeConverter<Character>";
-      }
-    });
-
-    convertToClasses(Matchers.subclassesOf(Enum.class), new TypeConverter() {
-      @SuppressWarnings("unchecked")
-      public Object convert(String value, TypeLiteral<?> toType) {
-        return Enum.valueOf((Class) toType.getRawType(), value);
-      }
-
-      @Override public String toString() {
-        return "TypeConverter<E extends Enum<E>>";
-      }
-    });
-
-    internalConvertToTypes(
-      new AbstractMatcher<TypeLiteral<?>>() {
-        public boolean matches(TypeLiteral<?> typeLiteral) {
-          return typeLiteral.getRawType() == Class.class;
-        }
-
-        @Override public String toString() {
-          return "Class<?>";
-        }
-      },
-      new TypeConverter() {
-        @SuppressWarnings("unchecked")
-        public Object convert(String value, TypeLiteral<?> toType) {
-          try {
-            return Class.forName(value);
-          }
-          catch (ClassNotFoundException e) {
-            throw new RuntimeException(e.getMessage());
-          }
-        }
-
-        @Override public String toString() {
-          return "TypeConverter<Class<?>>";
-        }
-      }
-    );
   }
 
-  private <T> void convertToPrimitiveType(Class<T> primitiveType,
-      final Class<T> wrapperType) {
+  /** Installs default converters for primitives, enums, and class literals. */
+  public void prepareBuiltInConverters(InjectorImpl injector) {
+    this.injector = injector;
+    try {
+      // Configure type converters.
+      convertToPrimitiveType(int.class, Integer.class);
+      convertToPrimitiveType(long.class, Long.class);
+      convertToPrimitiveType(boolean.class, Boolean.class);
+      convertToPrimitiveType(byte.class, Byte.class);
+      convertToPrimitiveType(short.class, Short.class);
+      convertToPrimitiveType(float.class, Float.class);
+      convertToPrimitiveType(double.class, Double.class);
+
+      convertToClass(Character.class, new TypeConverter() {
+        public Object convert(String value, TypeLiteral<?> toType) {
+          value = value.trim();
+          if (value.length() != 1) {
+            throw new RuntimeException("Length != 1.");
+          }
+          return value.charAt(0);
+        }
+
+        @Override public String toString() {
+          return "TypeConverter<Character>";
+        }
+      });
+
+      convertToClasses(Matchers.subclassesOf(Enum.class), new TypeConverter() {
+        @SuppressWarnings("unchecked")
+        public Object convert(String value, TypeLiteral<?> toType) {
+          return Enum.valueOf((Class) toType.getRawType(), value);
+        }
+
+        @Override public String toString() {
+          return "TypeConverter<E extends Enum<E>>";
+        }
+      });
+
+      internalConvertToTypes(
+        new AbstractMatcher<TypeLiteral<?>>() {
+          public boolean matches(TypeLiteral<?> typeLiteral) {
+            return typeLiteral.getRawType() == Class.class;
+          }
+
+          @Override public String toString() {
+            return "Class<?>";
+          }
+        },
+        new TypeConverter() {
+          @SuppressWarnings("unchecked")
+          public Object convert(String value, TypeLiteral<?> toType) {
+            try {
+              return Class.forName(value);
+            } catch (ClassNotFoundException e) {
+              throw new RuntimeException(e.getMessage());
+            }
+          }
+
+          @Override public String toString() {
+            return "TypeConverter<Class<?>>";
+          }
+        }
+      );
+    } finally {
+      this.injector = null;
+    }
+  }
+
+  private <T> void convertToPrimitiveType(Class<T> primitiveType, final Class<T> wrapperType) {
     try {
       final Method parser = wrapperType.getMethod(
           "parse" + Strings.capitalize(primitiveType.getName()), String.class);
@@ -117,11 +120,9 @@
         public Object convert(String value, TypeLiteral<?> toType) {
           try {
             return parser.invoke(null, value);
-          }
-          catch (IllegalAccessException e) {
+          } catch (IllegalAccessException e) {
             throw new AssertionError(e);
-          }
-          catch (InvocationTargetException e) {
+          } catch (InvocationTargetException e) {
             throw new RuntimeException(e.getTargetException().getMessage());
           }
         }
@@ -161,12 +162,12 @@
 
   private void internalConvertToTypes(Matcher<? super TypeLiteral<?>> typeMatcher,
       TypeConverter converter) {
-    state.addConverter(
+    injector.state.addConverter(
         new MatcherAndConverter(typeMatcher, converter, SourceProvider.UNKNOWN_SOURCE));
   }
 
   @Override public Boolean visitTypeConverterBinding(TypeConverterBinding command) {
-    state.addConverter(new MatcherAndConverter(
+    injector.state.addConverter(new MatcherAndConverter(
         command.getTypeMatcher(), command.getTypeConverter(), command.getSource()));
     return true;
   }
diff --git a/src/com/google/inject/internal/Errors.java b/src/com/google/inject/internal/Errors.java
index 1c6861d..b8de568 100644
--- a/src/com/google/inject/internal/Errors.java
+++ b/src/com/google/inject/internal/Errors.java
@@ -262,6 +262,10 @@
     return errorInUserCode("Error in custom provider, %s", runtimeException);
   }
 
+  public void exposedButNotBound(Key<?> key) {
+    addMessage("Could not expose() %s, it must be explicitly bound.", key);
+  }
+
   public static Collection<Message> getMessagesFromThrowable(Throwable throwable) {
     if (throwable instanceof ProvisionException) {
       return ((ProvisionException) throwable).getErrorMessages();
diff --git a/src/com/google/inject/internal/FailableCache.java b/src/com/google/inject/internal/FailableCache.java
index b252b31..3e3250f 100644
--- a/src/com/google/inject/internal/FailableCache.java
+++ b/src/com/google/inject/internal/FailableCache.java
@@ -45,7 +45,9 @@
       errors.merge((Errors) resultOrError);
       throw errors.toException();
     } else {
-      return (V) resultOrError;
+      @SuppressWarnings("unchecked") // create returned a non-error result, so this is safe
+      V result = (V) resultOrError;
+      return result;
     }
   }
 }
diff --git a/src/com/google/inject/internal/ModuleBinding.java b/src/com/google/inject/internal/ModuleBinding.java
index a714b68..6887359 100644
--- a/src/com/google/inject/internal/ModuleBinding.java
+++ b/src/com/google/inject/internal/ModuleBinding.java
@@ -27,6 +27,7 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.binder.AnnotatedBindingBuilder;
 import com.google.inject.binder.AnnotatedConstantBindingBuilder;
+import com.google.inject.binder.AnnotatedElementBuilder;
 import com.google.inject.binder.ConstantBindingBuilder;
 import com.google.inject.binder.LinkedBindingBuilder;
 import com.google.inject.binder.ScopedBindingBuilder;
@@ -36,6 +37,7 @@
 import com.google.inject.spi.ElementVisitor;
 import com.google.inject.spi.InjectionPoint;
 import com.google.inject.spi.Message;
+import com.google.inject.spi.PrivateEnvironment;
 import com.google.inject.util.Providers;
 import java.lang.annotation.Annotation;
 import java.util.Set;
@@ -153,6 +155,23 @@
     return new RegularBuilder(binder);
   }
 
+
+  /** @param binder the binder where errors will be reported. */
+  public ExposureBuilder<T> exposedKeyBuilder(Binder binder,
+      final PrivateEnvironment privateEnvironment) {
+    if (target != EMPTY_TARGET) {
+      throw new AssertionError();
+    }
+
+    target = new Target<T>() {
+      public <V> V acceptTargetVisitor(BindingTargetVisitor<? super T, V> visitor) {
+        return visitor.visitExposed(privateEnvironment);
+      }
+    };
+
+    return new ExposureBuilder<T>(this, binder);
+  }
+
   /**
    * Write access to the internal state of this element. Not for use by the public API.
    */
@@ -163,8 +182,7 @@
       this.binder = binder.skipSources(RegularBuilder.class);
     }
 
-    public LinkedBindingBuilder<T> annotatedWith(
-        Class<? extends Annotation> annotationType) {
+    public LinkedBindingBuilder<T> annotatedWith(Class<? extends Annotation> annotationType) {
       checkNotNull(annotationType, "annotationType");
       checkNotAnnotated();
       key = Key.get(key.getTypeLiteral(), annotationType);
@@ -352,8 +370,7 @@
   /**
    * Package-private write access to the internal state of this element.
    */
-  class ConstantBuilder
-      implements AnnotatedConstantBindingBuilder, ConstantBindingBuilder {
+  class ConstantBuilder implements AnnotatedConstantBindingBuilder, ConstantBindingBuilder {
     private final Binder binder;
 
     ConstantBuilder(Binder binder) {
@@ -455,6 +472,45 @@
     }
   }
 
+  /**
+   * For private binder's expose() method.
+   */
+  public static class ExposureBuilder<T> implements AnnotatedElementBuilder {
+    private ModuleBinding<T> binding;
+    private final Binder binder;
+
+    public ExposureBuilder(ModuleBinding<T> binding, Binder binder) {
+      this.binding = binding;
+      this.binder = binder;
+    }
+
+    public void annotatedWith(Class<? extends Annotation> annotationType) {
+      checkNotNull(annotationType, "annotationType");
+      checkNotAnnotated();
+      binding.key = Key.get(binding.key.getTypeLiteral(), annotationType);
+    }
+
+    public void annotatedWith(Annotation annotation) {
+      checkNotNull(annotation, "annotation");
+      checkNotAnnotated();
+      binding.key = Key.get(binding.key.getTypeLiteral(), annotation);
+    }
+
+    public Key<?> getKey() {
+      return binding.key;
+    }
+
+    private void checkNotAnnotated() {
+      if (binding.key.getAnnotationType() != null) {
+        binder.addError(ANNOTATION_ALREADY_SPECIFIED);
+      }
+    }
+
+    @Override public String toString() {
+      return "AnnotatedElementBuilder";
+    }
+  }
+
   /** A binding target, which provides instances from a specific key. */
   private interface Target<T> {
     <V> V acceptTargetVisitor(BindingTargetVisitor<? super T, V> visitor);
diff --git a/src/com/google/inject/internal/ProviderMethod.java b/src/com/google/inject/internal/ProviderMethod.java
index 659e4d0..6514d2c 100644
--- a/src/com/google/inject/internal/ProviderMethod.java
+++ b/src/com/google/inject/internal/ProviderMethod.java
@@ -16,12 +16,14 @@
 
 package com.google.inject.internal;
 
-import com.google.inject.Provider;
-import com.google.inject.Key;
 import com.google.inject.Binder;
+import com.google.inject.Exposed;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.binder.PrivateBinder;
 import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
 import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.List;
 
 /**
@@ -37,6 +39,7 @@
   private final Object instance;
   private final Method method;
   private final List<Provider<?>> parameterProviders;
+  private final boolean exposed;
 
   /**
    * @param method the method to invoke. It's return type must be the same type as {@code key}.
@@ -48,6 +51,7 @@
     this.instance = instance;
     this.method = method;
     this.parameterProviders = parameterProviders;
+    this.exposed = method.isAnnotationPresent(Exposed.class);
 
     method.setAccessible(true);
   }
@@ -61,10 +65,16 @@
   }
 
   public void configure(Binder binder) {
+    binder = binder.withSource(method);
+
     if (scopeAnnotation != null) {
-      binder.withSource(method).bind(key).toProvider(this).in(scopeAnnotation);
+      binder.bind(key).toProvider(this).in(scopeAnnotation);
     } else {
-      binder.withSource(method).bind(key).toProvider(this);
+      binder.bind(key).toProvider(this);
+    }
+
+    if (exposed) {
+      ((PrivateBinder) binder).expose(key);
     }
   }
 
diff --git a/src/com/google/inject/internal/ProviderMethodsModule.java b/src/com/google/inject/internal/ProviderMethodsModule.java
index 486abd9..1b873cb 100644
--- a/src/com/google/inject/internal/ProviderMethodsModule.java
+++ b/src/com/google/inject/internal/ProviderMethodsModule.java
@@ -16,7 +16,6 @@
 
 package com.google.inject.internal;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import com.google.common.collect.Lists;
 import com.google.inject.Binder;
@@ -57,31 +56,9 @@
       return Modules.EMPTY_MODULE;
     }
 
-    // don't install provider methods for private modules, they take care of that manually
-    if (isPrivateModule(module)) {
-      return Modules.EMPTY_MODULE;
-    }
-
     return new ProviderMethodsModule(module);
   }
 
-  private static boolean isPrivateModule(Module module) {
-    // use the ugly class name to avoid an even uglier dependency. If private modules ever get
-    // incorporated into core, we could use a single instanceof instead of this loop
-    for (Class<?> c = module.getClass(); c != Object.class; c = c.getSuperclass()) {
-      if (c.getName().equals("com.google.inject.privatemodules.PrivateModule")) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  /** See {@link com.google.inject.privatemodules.PrivateModule}. */
-  public static ProviderMethodsModule forPrivateModule(Module privateModule) {
-    checkArgument(isPrivateModule(privateModule));
-    return new ProviderMethodsModule(privateModule);
-  }
-
   public synchronized void configure(Binder binder) {
     for (ProviderMethod<?> providerMethod : getProviderMethods(binder)) {
       providerMethod.configure(binder);
diff --git a/src/com/google/inject/spi/BindingTargetVisitor.java b/src/com/google/inject/spi/BindingTargetVisitor.java
index 3775246..bf69009 100644
--- a/src/com/google/inject/spi/BindingTargetVisitor.java
+++ b/src/com/google/inject/spi/BindingTargetVisitor.java
@@ -105,4 +105,9 @@
    *      com.google.inject.Injector#getBinding(Key) Injector.getBinding(provided)}
    */
   V visitProviderBinding(Key<?> provided);
+
+  /**
+   * Visit a binding to a key exposed by a private environment.
+   */
+  V visitExposed(PrivateEnvironment privateEnvironment);
 }
diff --git a/src/com/google/inject/spi/DefaultBindingTargetVisitor.java b/src/com/google/inject/spi/DefaultBindingTargetVisitor.java
index 245a3b8..c213dbc 100644
--- a/src/com/google/inject/spi/DefaultBindingTargetVisitor.java
+++ b/src/com/google/inject/spi/DefaultBindingTargetVisitor.java
@@ -69,4 +69,8 @@
   public V visitProviderBinding(Key<?> provided) {
     return visitOther();
   }
+
+  public V visitExposed(PrivateEnvironment privateEnvironment) {
+    return visitOther();
+  }
 }
diff --git a/src/com/google/inject/spi/DefaultElementVisitor.java b/src/com/google/inject/spi/DefaultElementVisitor.java
index 1d2e21d..9ab49ab 100644
--- a/src/com/google/inject/spi/DefaultElementVisitor.java
+++ b/src/com/google/inject/spi/DefaultElementVisitor.java
@@ -69,11 +69,7 @@
     return visitElement(staticInjectionRequest);
   }
 
-  public V visitPrivateElements(PrivateEnvironment privateEnvironment) {
+  public V visitPrivateEnvironment(PrivateEnvironment privateEnvironment) {
     return visitElement(privateEnvironment);
   }
-
-  public V visitExposure(Exposure exposure) {
-    return visitElement(exposure);
-  }
 }
diff --git a/src/com/google/inject/spi/ElementVisitor.java b/src/com/google/inject/spi/ElementVisitor.java
index b091a37..19b1e8a 100644
--- a/src/com/google/inject/spi/ElementVisitor.java
+++ b/src/com/google/inject/spi/ElementVisitor.java
@@ -72,10 +72,5 @@
   /**
    * Visit a collection of configuration elements for a private environment.
    */
-  V visitPrivateElements(PrivateEnvironment privateEnvironment);
-
-  /**
-   * Visit the exposure of a binding to its enclosing environment.
-   */
-  V visitExposure(Exposure exposure);
+  V visitPrivateEnvironment(PrivateEnvironment privateEnvironment);
 }
diff --git a/src/com/google/inject/spi/Elements.java b/src/com/google/inject/spi/Elements.java
index ce2a0b0..6d1ae25 100644
--- a/src/com/google/inject/spi/Elements.java
+++ b/src/com/google/inject/spi/Elements.java
@@ -25,6 +25,7 @@
 import com.google.inject.Binder;
 import com.google.inject.Key;
 import com.google.inject.Module;
+import com.google.inject.PrivateModule;
 import com.google.inject.Provider;
 import com.google.inject.Scope;
 import com.google.inject.Stage;
@@ -35,6 +36,7 @@
 import com.google.inject.binder.PrivateBinder;
 import com.google.inject.internal.Errors;
 import com.google.inject.internal.ModuleBinding;
+import com.google.inject.internal.ModuleBinding.ExposureBuilder;
 import com.google.inject.internal.ProviderMethodsModule;
 import com.google.inject.internal.SourceProvider;
 import com.google.inject.matcher.Matcher;
@@ -109,6 +111,9 @@
     private final List<Element> elements;
     private final Object source;
     private final SourceProvider sourceProvider;
+
+    /** The binder where exposed bindings will be created */
+    private final RecordingBinder parent;
     private final PrivateEnvironment privateEnvironment;
 
     private RecordingBinder(Stage stage) {
@@ -118,28 +123,32 @@
       this.source = null;
       this.sourceProvider = new SourceProvider()
           .plusSkippedClasses(Elements.class, RecordingBinder.class, AbstractModule.class);
+      this.parent = null;
       this.privateEnvironment = null;
     }
 
-    /** Creates a recording binder that's backed by {@code backingBinder}. */
-    private RecordingBinder(RecordingBinder parent, Object source, SourceProvider sourceProvider) {
+    /** Creates a recording binder that's backed by {@code prototype}. */
+    private RecordingBinder(
+        RecordingBinder prototype, Object source, SourceProvider sourceProvider) {
       checkArgument(source == null ^ sourceProvider == null);
 
-      this.stage = parent.stage;
-      this.modules = parent.modules;
-      this.elements = parent.elements;
+      this.stage = prototype.stage;
+      this.modules = prototype.modules;
+      this.elements = prototype.elements;
       this.source = source;
       this.sourceProvider = sourceProvider;
-      this.privateEnvironment = parent.privateEnvironment;
+      this.parent = prototype.parent;
+      this.privateEnvironment = prototype.privateEnvironment;
     }
 
     /** Creates a private recording binder. */
     private RecordingBinder(RecordingBinder parent, PrivateEnvironment privateEnvironment) {
       this.stage = parent.stage;
       this.modules = Sets.newHashSet();
-      this.elements = privateEnvironment.elementsMutable;
+      this.elements = privateEnvironment.getElementsMutable();
       this.source = parent.source;
       this.sourceProvider = parent.sourceProvider;
+      this.parent = parent;
       this.privateEnvironment = privateEnvironment;
     }
 
@@ -168,8 +177,13 @@
 
     public void install(Module module) {
       if (modules.add(module)) {
+        Binder binder = this;
+        if (module instanceof PrivateModule) {
+          binder = binder.newPrivateBinder();
+        }
+
         try {
-          module.configure(this);
+          module.configure(binder);
         } catch (RuntimeException e) {
           Collection<Message> messages = Errors.getMessagesFromThrowable(e);
           if (!messages.isEmpty()) {
@@ -178,7 +192,7 @@
             addError(e);
           }
         }
-        install(ProviderMethodsModule.forModule(module));
+        binder.install(ProviderMethodsModule.forModule(module));
       }
     }
 
@@ -277,15 +291,18 @@
       return exposeInternal(Key.get(type));
     }
 
-    private AnnotatedElementBuilder exposeInternal(Key<?> key) {
+    private <T> AnnotatedElementBuilder exposeInternal(Key<T> key) {
       if (privateEnvironment == null) {
         throw new UnsupportedOperationException("expose() only supported on PrivateBinder. "
             + "Avoid using 'instanceof PrivateBinder', it's unsafe.");
       }
 
-      Exposure exposure = new Exposure(getSource(), privateEnvironment, key);
-      elements.add(exposure);
-      return exposure.annotatedElementBuilder(this);
+      ModuleBinding<T> exposeBinding = new ModuleBinding<T>(getSource(), key);
+      parent.elements.add(exposeBinding);
+
+      ExposureBuilder<T> builder = exposeBinding.exposedKeyBuilder(this, privateEnvironment);
+      privateEnvironment.addExposureBuilder(builder);
+      return builder;
     }
 
     protected Object getSource() {
diff --git a/src/com/google/inject/spi/Exposure.java b/src/com/google/inject/spi/Exposure.java
index 07a6039..d9d0370 100644
--- a/src/com/google/inject/spi/Exposure.java
+++ b/src/com/google/inject/spi/Exposure.java
@@ -1,88 +1,88 @@
-/**
- * Copyright (C) 2008 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.inject.spi;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-import com.google.inject.Binder;
-import com.google.inject.Key;
-import com.google.inject.binder.AnnotatedElementBuilder;
-import com.google.inject.internal.ModuleBinding;
-import java.lang.annotation.Annotation;
-
-/**
- * Exposes a binding to its enclosing environment.
- *
- * @author jessewilson@google.com (Jesse Wilson)
- * @since 2.0
- */
-public final class Exposure implements Element {
-  private final Object source;
-  private final PrivateEnvironment privateEnvironment;
-  private Key<?> key;
-
-  Exposure(Object source, PrivateEnvironment privateEnvironment, Key<?> key) {
-    this.source = checkNotNull(source, "source");
-    this.privateEnvironment = privateEnvironment;
-    this.key = checkNotNull(key, "key");
-  }
-
-  public Object getSource() {
-    return source;
-  }
-
-  /**
-   * Returns the environment that owns this binding. Its enclosing environment gains access to the
-   * binding via this exposure.
-   */
-  public PrivateEnvironment getPrivateEnvironment() {
-    return privateEnvironment;
-  }
-
-  /**
-   * Returns the exposed key.
-   */
-  public Key<?> getKey() {
-    return key;
-  }
-
-  public <T> T acceptVisitor(ElementVisitor<T> visitor) {
-    return visitor.visitExposure(this);
-  }
-
-  /** Returns a builder that applies annotations to this exposed key. */
-  AnnotatedElementBuilder annotatedElementBuilder(final Binder binder) {
-    return new AnnotatedElementBuilder() {
-      public void annotatedWith(Class<? extends Annotation> annotationType) {
-        checkNotNull(annotationType, "annotationType");
-        checkNotAnnotated();
-        key = Key.get(key.getTypeLiteral(), annotationType);
-      }
-
-      public void annotatedWith(Annotation annotation) {
-        checkNotNull(annotation, "annotation");
-        checkNotAnnotated();
-        key = Key.get(key.getTypeLiteral(), annotation);
-      }
-
-      private void checkNotAnnotated() {
-        if (key.getAnnotationType() != null) {
-          binder.addError(ModuleBinding.ANNOTATION_ALREADY_SPECIFIED);
-        }
-      }
-    };
-  }
-}
+///**
+// * Copyright (C) 2008 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.inject.spi;
+//
+//import static com.google.common.base.Preconditions.checkNotNull;
+//import com.google.inject.Binder;
+//import com.google.inject.Key;
+//import com.google.inject.binder.AnnotatedElementBuilder;
+//import com.google.inject.internal.ModuleBinding;
+//import java.lang.annotation.Annotation;
+//
+///**
+// * Exposes a binding to its enclosing environment.
+// *
+// * @author jessewilson@google.com (Jesse Wilson)
+// * @since 2.0
+// */
+//public final class Exposure implements Element {
+//  private final Object source;
+//  private final PrivateEnvironment privateEnvironment;
+//  private Key<?> key;
+//
+//  Exposure(Object source, PrivateEnvironment privateEnvironment, Key<?> key) {
+//    this.source = checkNotNull(source, "source");
+//    this.privateEnvironment = privateEnvironment;
+//    this.key = checkNotNull(key, "key");
+//  }
+//
+//  public Object getSource() {
+//    return source;
+//  }
+//
+//  /**
+//   * Returns the environment that owns this binding. Its enclosing environment gains access to the
+//   * binding via this exposure.
+//   */
+//  public PrivateEnvironment getPrivateEnvironment() {
+//    return privateEnvironment;
+//  }
+//
+//  /**
+//   * Returns the exposed key.
+//   */
+//  public Key<?> getKey() {
+//    return key;
+//  }
+//
+//  public <T> T acceptVisitor(ElementVisitor<T> visitor) {
+//    return visitor.visitExposure(this);
+//  }
+//
+//  /** Returns a builder that applies annotations to this exposed key. */
+//  AnnotatedElementBuilder annotatedElementBuilder(final Binder binder) {
+//    return new AnnotatedElementBuilder() {
+//      public void annotatedWith(Class<? extends Annotation> annotationType) {
+//        checkNotNull(annotationType, "annotationType");
+//        checkNotAnnotated();
+//        key = Key.get(key.getTypeLiteral(), annotationType);
+//      }
+//
+//      public void annotatedWith(Annotation annotation) {
+//        checkNotNull(annotation, "annotation");
+//        checkNotAnnotated();
+//        key = Key.get(key.getTypeLiteral(), annotation);
+//      }
+//
+//      private void checkNotAnnotated() {
+//        if (key.getAnnotationType() != null) {
+//          binder.addError(ModuleBinding.ANNOTATION_ALREADY_SPECIFIED);
+//        }
+//      }
+//    };
+//  }
+//}
diff --git a/src/com/google/inject/spi/ModuleWriter.java b/src/com/google/inject/spi/ModuleWriter.java
index 2ce1e3c..a9a4cd0 100644
--- a/src/com/google/inject/spi/ModuleWriter.java
+++ b/src/com/google/inject/spi/ModuleWriter.java
@@ -16,19 +16,21 @@
 
 package com.google.inject.spi;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import com.google.common.collect.Maps;
 import com.google.inject.Binder;
 import com.google.inject.Binding;
 import com.google.inject.Key;
 import com.google.inject.Module;
 import com.google.inject.Provider;
 import com.google.inject.Scope;
-import com.google.inject.binder.LinkedBindingBuilder;
 import com.google.inject.binder.PrivateBinder;
 import com.google.inject.binder.ScopedBindingBuilder;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Constructor;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import org.aopalliance.intercept.MethodInterceptor;
 
@@ -40,9 +42,10 @@
  */
 public class ModuleWriter {
 
+  private final Map<PrivateEnvironment, PrivateBinder> environmentToBinder = Maps.newHashMap();
+
   /**
-   * Returns a module that executes the specified elements
-   * using this executing visitor.
+   * Returns a module that executes the specified elements using this executing visitor.
    */
   public final Module create(final Iterable<? extends Element> elements) {
     return new Module() {
@@ -101,15 +104,10 @@
         return null;
       }
 
-      public Void visitPrivateElements(PrivateEnvironment privateEnvironment) {
+      public Void visitPrivateEnvironment(PrivateEnvironment privateEnvironment) {
         writePrivateElements(binder, privateEnvironment);
         return null;
       }
-
-      public Void visitExposure(Exposure expose) {
-        writeExpose(binder, expose);
-        return null;
-      }
     };
 
     for (Element element : elements) {
@@ -148,47 +146,55 @@
   }
 
   protected <T> void writeBind(Binder binder, Binding<T> element) {
-    LinkedBindingBuilder<T> lbb = binder.withSource(element.getSource()).bind(element.getKey());
-    ScopedBindingBuilder sbb = applyTarget(element, lbb);
+    ScopedBindingBuilder sbb
+        = bindKeyToTarget(element, binder.withSource(element.getSource()), element.getKey());
     applyScoping(element, sbb);
   }
 
+  /**
+   * Writes all the elements bound in the private environment to new private binder that's enclosed
+   * by the current binder. The private binder will be associated with its environment, so exposed
+   * bindings from the main elements list can be exposed from the corresponding private binder.
+   */
   protected void writePrivateElements(Binder binder, PrivateEnvironment element) {
     PrivateBinder privateBinder = binder.withSource(element.getSource()).newPrivateBinder();
+    setPrivateBinder(element, privateBinder);
     apply(privateBinder, element.getElements());
   }
 
-  protected void writeExpose(Binder binder, Exposure exposure) {
-    PrivateBinder privateBinder = (PrivateBinder) binder;
-    privateBinder.withSource(exposure.getSource()).expose(exposure.getKey());
-  }
-
   /**
    * Execute this target against the linked binding builder.
    */
-  protected <T> ScopedBindingBuilder applyTarget(Binding<T> binding,
-      final LinkedBindingBuilder<T> linkedBindingBuilder) {
+  protected <T> ScopedBindingBuilder bindKeyToTarget(
+      final Binding<T> binding, final Binder binder, final Key<T> key) {
     return binding.acceptTargetVisitor(new BindingTargetVisitor<T, ScopedBindingBuilder>() {
       public ScopedBindingBuilder visitInstance(T instance, Set<InjectionPoint> injectionPoints) {
-        linkedBindingBuilder.toInstance(instance);
+        binder.bind(key).toInstance(instance);
         return null;
       }
 
       public ScopedBindingBuilder visitProvider(Provider<? extends T> provider,
           Set<InjectionPoint> injectionPoints) {
-        return linkedBindingBuilder.toProvider(provider);
+        return binder.bind(key).toProvider(provider);
       }
 
-      public ScopedBindingBuilder visitProviderKey(Key<? extends Provider<? extends T>> providerKey) {
-        return linkedBindingBuilder.toProvider(providerKey);
+      public ScopedBindingBuilder visitProviderKey(
+          Key<? extends Provider<? extends T>> providerKey) {
+        return binder.bind(key).toProvider(providerKey);
       }
 
-      public ScopedBindingBuilder visitKey(Key<? extends T> key) {
-        return linkedBindingBuilder.to(key);
+      public ScopedBindingBuilder visitKey(Key<? extends T> targetKey) {
+        return binder.bind(key).to(targetKey);
       }
 
       public ScopedBindingBuilder visitUntargetted() {
-        return linkedBindingBuilder;
+        return binder.bind(key);
+      }
+
+      public ScopedBindingBuilder visitExposed(PrivateEnvironment privateEnvironment) {
+        PrivateBinder privateBinder = getPrivateBinder(privateEnvironment);
+        privateBinder.withSource(binding.getSource()).expose(key);
+        return null;
       }
 
       public ScopedBindingBuilder visitConvertedConstant(T value) {
@@ -206,6 +212,26 @@
     });
   }
 
+  /**
+   * Associates {@code binder} with {@code privateEnvironment}. This can later be used to lookup the
+   * binder for its environment.
+   */
+  protected void setPrivateBinder(PrivateEnvironment privateEnvironment, PrivateBinder binder) {
+    checkArgument(!environmentToBinder.containsKey(privateEnvironment),
+        "A private binder already exists for %s", privateEnvironment);
+    environmentToBinder.put(privateEnvironment, binder);
+  }
+
+  /**
+   * Returns the {@code binder} accociated with {@code privateEnvironment}. This can be used to
+   * expose bindings from {@code privateEnvironment} from the corresponding private binder.
+   */
+  protected PrivateBinder getPrivateBinder(PrivateEnvironment privateEnvironment) {
+    PrivateBinder privateBinder = environmentToBinder.get(privateEnvironment);
+    checkArgument(privateBinder != null, "No private binder for %s", privateEnvironment);
+    return privateBinder;
+  }
+
   protected void applyScoping(Binding<?> binding, final ScopedBindingBuilder scopedBindingBuilder) {
     binding.acceptScopingVisitor(new BindingScopingVisitor<Void>() {
       public Void visitEagerSingleton() {
diff --git a/src/com/google/inject/spi/PrivateEnvironment.java b/src/com/google/inject/spi/PrivateEnvironment.java
index 546624b..c757102 100644
--- a/src/com/google/inject/spi/PrivateEnvironment.java
+++ b/src/com/google/inject/spi/PrivateEnvironment.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.inject.Key;
+import com.google.inject.internal.ModuleBinding.ExposureBuilder;
 import java.util.List;
 import java.util.Set;
 
@@ -33,8 +34,16 @@
  */
 public final class PrivateEnvironment implements Element {
 
+  /*
+   * This class acts as both a value object and as a builder. When getElements() is called, an
+   * immutable collection of elements is constructed and the original mutable list is nulled out.
+   * Similarly, the exposed keys are made immutable on access.
+   */
+
   private final Object source;
-  List<Element> elementsMutable = Lists.newArrayList();
+
+  private List<Element> elementsMutable = Lists.newArrayList();
+  private List<ExposureBuilder<?>> exposureBuilders = Lists.newArrayList();
 
   /** lazily instantiated */
   private ImmutableList<Element> elements;
@@ -51,8 +60,7 @@
   }
 
   /**
-   * Returns the configuration information in this private environment, including the {@link
-   * Exposure} elements that make configuration available to the enclosing environment.
+   * Returns the configuration information in this private environment.
    */
   public List<Element> getElements() {
     if (elements == null) {
@@ -69,18 +77,25 @@
   public Set<Key<?>> getExposedKeys() {
     if (exposedKeys == null) {
       Set<Key<?>> exposedKeysMutable = Sets.newLinkedHashSet();
-      for (Element element : getElements()) {
-        if (element instanceof Exposure) {
-          exposedKeysMutable.add(((Exposure) element).getKey());
-        }
+      for (ExposureBuilder<?> exposureBuilder : exposureBuilders) {
+        exposedKeysMutable.add(exposureBuilder.getKey());
       }
       exposedKeys = ImmutableSet.copyOf(exposedKeysMutable);
+      exposureBuilders = null;
     }
 
     return exposedKeys;
   }
 
   public <T> T acceptVisitor(ElementVisitor<T> visitor) {
-    return visitor.visitPrivateElements(this);
+    return visitor.visitPrivateEnvironment(this);
+  }
+
+  List<Element> getElementsMutable() {
+    return elementsMutable;
+  }
+
+  void addExposureBuilder(ExposureBuilder<?> exposureBuilder) {
+    exposureBuilders.add(exposureBuilder);
   }
 }
diff --git a/test/com/google/inject/AllTests.java b/test/com/google/inject/AllTests.java
index e61332c..d6fb5dc 100644
--- a/test/com/google/inject/AllTests.java
+++ b/test/com/google/inject/AllTests.java
@@ -66,6 +66,7 @@
     suite.addTestSuite(OptionalBindingTest.class);
     suite.addTestSuite(OverrideModuleTest.class);
     suite.addTestSuite(ParentInjectorTest.class);
+    suite.addTestSuite(PrivateModuleTest.class);
     suite.addTestSuite(ProviderInjectionTest.class);
     suite.addTestSuite(ProviderMethodsTest.class);
     suite.addTestSuite(ProvisionExceptionTest.class);
diff --git a/extensions/privatemodules/test/com/google/inject/privatemodules/PrivateModuleTest.java b/test/com/google/inject/PrivateModuleTest.java
similarity index 84%
rename from extensions/privatemodules/test/com/google/inject/privatemodules/PrivateModuleTest.java
rename to test/com/google/inject/PrivateModuleTest.java
index 031a3af..39e32a0 100644
--- a/extensions/privatemodules/test/com/google/inject/privatemodules/PrivateModuleTest.java
+++ b/test/com/google/inject/PrivateModuleTest.java
@@ -14,18 +14,10 @@
  * limitations under the License.
  */
 
-package com.google.inject.privatemodules;
+package com.google.inject;
 
-import com.google.inject.AbstractModule;
 import static com.google.inject.Asserts.assertContains;
-import com.google.inject.CreationException;
-import com.google.inject.Guice;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Key;
-import com.google.inject.Provider;
-import com.google.inject.Provides;
-import com.google.inject.ProvisionException;
+import com.google.inject.binder.PrivateBinder;
 import com.google.inject.name.Named;
 import static com.google.inject.name.Names.named;
 import junit.framework.TestCase;
@@ -68,6 +60,25 @@
     assertEquals("public", ab2.a);
     assertEquals("ii", ab2.b);
   }
+  
+  public void testWithoutPrivateModules() {
+    Injector injector = Guice.createInjector(new AbstractModule() {
+      protected void configure() {
+        PrivateBinder bindA = binder().newPrivateBinder();
+        bindA.bind(String.class).annotatedWith(named("a")).toInstance("i");
+        bindA.expose(String.class).annotatedWith(named("a"));
+        bindA.bind(String.class).annotatedWith(named("c")).toInstance("private to A");
+
+        PrivateBinder bindB = binder().newPrivateBinder();
+        bindB.bind(String.class).annotatedWith(named("b")).toInstance("ii");
+        bindB.expose(String.class).annotatedWith(named("b"));
+        bindB.bind(String.class).annotatedWith(named("c")).toInstance("private to B");
+      }
+    });
+
+    assertEquals("i", injector.getInstance(Key.get(String.class, named("a"))));
+    assertEquals("ii", injector.getInstance(Key.get(String.class, named("b"))));
+  }
 
   public void testPrivateModulesAndProvidesMethods() {
     Injector injector = Guice.createInjector(new AbstractModule() {
@@ -89,11 +100,11 @@
         install(new PrivateModule() {
           public void configurePrivateBindings() {}
 
-          @Provides @Named("a") String providePrivateA() {
+          @Provides @Named("c") String providePrivateC() {
             return "private";
           }
 
-          @Provides @Exposed @Named("b") String providePublicB() {
+          @Provides @Exposed @Named("d") String providePublicD() {
             return "ii";
           }
         });
@@ -101,7 +112,7 @@
     });
 
     assertEquals("i", injector.getInstance(Key.get(String.class, named("a"))));
-    assertEquals("ii", injector.getInstance(Key.get(String.class, named("b"))));
+    assertEquals("ii", injector.getInstance(Key.get(String.class, named("d"))));
   }
 
   public void testCannotBindAKeyExportedByASibling() {
@@ -122,9 +133,12 @@
           });
         }
       });
-      fail("KNOWN ISSUE: Binding to 'private' should conflict with binding to 'public'");
+      fail();
     } catch (CreationException expected) {
-      assertContains(expected.getMessage(), "Cannot bind String");
+      assertContains(expected.getMessage(),
+          "A binding to java.lang.String was already configured at ",
+          getClass().getName(), ".configurePrivateBindings(PrivateModuleTest.java:",
+          " at " + getClass().getName(), ".configurePrivateBindings(PrivateModuleTest.java:");
     }
   }
 
@@ -144,9 +158,9 @@
       });
       fail("AB was exposed but not bound");
     } catch (CreationException expected) {
-      assertContains(expected.getMessage(), "Could not expose() at ",
-          PrivateModuleTest.class.getName(), ".configurePrivateBindings(PrivateModuleTest.java:",
-          Key.get(AB.class).toString(), " must be explicitly bound.");
+      assertContains(expected.getMessage(),
+          "Could not expose() " + AB.class.getName() + ", it must be explicitly bound",
+          ".configurePrivateBindings(PrivateModuleTest.java:");
     }
   }
 
@@ -232,21 +246,21 @@
       }
     });
 
-    assertEquals("boeo", injector.getInstance(
-        Key.get(String.class, named("bound outer, exposed outer"))));
-    assertEquals("bieb", injector.getInstance(
-        Key.get(String.class, named("bound inner, exposed both"))));
+    assertEquals("boeo",
+        injector.getInstance(Key.get(String.class, named("bound outer, exposed outer"))));
+    assertEquals("bieb",
+        injector.getInstance(Key.get(String.class, named("bound inner, exposed both"))));
 
     try {
       injector.getInstance(Key.get(String.class, named("bound outer, exposed none")));
       fail();
-    } catch (ProvisionException expected) {
+    } catch (ConfigurationException expected) {
     }
 
     try {
       injector.getInstance(Key.get(String.class, named("bound inner, exposed none")));
       fail();
-    } catch (ProvisionException expected) {
+    } catch (ConfigurationException expected) {
     }
   }
 
diff --git a/test/com/google/inject/spi/ElementsTest.java b/test/com/google/inject/spi/ElementsTest.java
index 09b51d1..08bd863 100644
--- a/test/com/google/inject/spi/ElementsTest.java
+++ b/test/com/google/inject/spi/ElementsTest.java
@@ -672,6 +672,14 @@
   }
 
   public void testNewPrivateBinder() {
+    final Key<Collection> collection = Key.get(Collection.class, SampleAnnotation.class);
+    final Key<ArrayList> arrayList = Key.get(ArrayList.class);
+    final ImmutableSet<Key<?>> collections = ImmutableSet.<Key<?>>of(arrayList, collection);
+
+    final Key<?> a = Key.get(String.class, Names.named("a"));
+    final Key<?> b = Key.get(String.class, Names.named("b"));
+    final ImmutableSet<Key<?>> ab = ImmutableSet.of(a, b);
+
     checkModule(
         new AbstractModule() {
           protected void configure() {
@@ -683,30 +691,16 @@
             PrivateBinder two = binder().withSource("1 ElementsTest.java")
                 .newPrivateBinder().withSource("2 ElementsTest.java");
             two.expose(String.class).annotatedWith(Names.named("a"));
-            two.expose(Key.get(String.class, Names.named("b")));
+            two.expose(b);
             two.bind(List.class).to(ArrayList.class);
           }
         },
 
         new FailingElementVisitor() {
-          @Override public Void visitPrivateElements(PrivateEnvironment one) {
-            assertEquals(ImmutableSet.<Key<?>>of(Key.get(ArrayList.class),
-                Key.get(Collection.class, SampleAnnotation.class)), one.getExposedKeys());
+          @Override public Void visitPrivateEnvironment(PrivateEnvironment one) {
+            assertEquals(collections, one.getExposedKeys());
             checkElements(one.getElements(),
                 new FailingElementVisitor() {
-                  @Override public Void visitExposure(Exposure exposure) {
-                    assertEquals(Key.get(ArrayList.class), exposure.getKey());
-                    return null;
-                  }
-                },
-                new FailingElementVisitor() {
-                  @Override public Void visitExposure(Exposure exposure) {
-                    assertEquals(Key.get(Collection.class, SampleAnnotation.class),
-                        exposure.getKey());
-                    return null;
-                  }
-                },
-                new FailingElementVisitor() {
                   @Override public <T> Void visitBinding(Binding<T> binding) {
                     assertEquals(Key.get(List.class), binding.getKey());
                     return null;
@@ -718,26 +712,37 @@
         },
 
         new FailingElementVisitor() {
-          @Override public Void visitPrivateElements(PrivateEnvironment two) {
-            assertEquals(ImmutableSet.<Key<?>>of(Key.get(String.class, Names.named("a")),
-                Key.get(String.class, Names.named("b"))), two.getExposedKeys());
+          @Override public <T> Void visitBinding(Binding<T> binding) {
+            assertEquals(arrayList, binding.getKey());
+            binding.acceptTargetVisitor(new FailingTargetVisitor<T>() {
+              @Override public Void visitExposed(PrivateEnvironment privateEnvironment) {
+                assertEquals(collections, privateEnvironment.getExposedKeys());
+                return null;
+              }
+            });
+            return null;
+          }
+        },
+
+        new FailingElementVisitor() {
+          @Override public <T> Void visitBinding(Binding<T> binding) {
+            assertEquals(collection, binding.getKey());
+            binding.acceptTargetVisitor(new FailingTargetVisitor<T>() {
+              @Override public Void visitExposed(PrivateEnvironment privateEnvironment) {
+                assertEquals(collections, privateEnvironment.getExposedKeys());
+                return null;
+              }
+            });
+            return null;
+          }
+        },
+
+        new FailingElementVisitor() {
+          @Override public Void visitPrivateEnvironment(PrivateEnvironment two) {
+            assertEquals(ab, two.getExposedKeys());
             assertEquals("1 ElementsTest.java", two.getSource());
             checkElements(two.getElements(),
                 new FailingElementVisitor() {
-                  @Override public Void visitExposure(Exposure exposure) {
-                    assertEquals("2 ElementsTest.java", exposure.getSource());
-                    assertEquals(Key.get(String.class, Names.named("a")), exposure.getKey());
-                    return null;
-                  }
-                },
-                new FailingElementVisitor() {
-                  @Override public Void visitExposure(Exposure exposure) {
-                    assertEquals("2 ElementsTest.java", exposure.getSource());
-                    assertEquals(Key.get(String.class, Names.named("b")), exposure.getKey());
-                    return null;
-                  }
-                },
-                new FailingElementVisitor() {
                   @Override public <T> Void visitBinding(Binding<T> binding) {
                     assertEquals("2 ElementsTest.java", binding.getSource());
                     assertEquals(Key.get(List.class), binding.getKey());
@@ -747,6 +752,34 @@
             );
             return null;
           }
+        },
+
+        new FailingElementVisitor() {
+          @Override public <T> Void visitBinding(Binding<T> binding) {
+            assertEquals(a, binding.getKey());
+            assertEquals("2 ElementsTest.java", binding.getSource());
+            binding.acceptTargetVisitor(new FailingTargetVisitor<T>() {
+              @Override public Void visitExposed(PrivateEnvironment privateEnvironment) {
+                assertEquals(ab, privateEnvironment.getExposedKeys());
+                return null;
+              }
+            });
+            return null;
+          }
+        },
+
+        new FailingElementVisitor() {
+          @Override public <T> Void visitBinding(Binding<T> binding) {
+            assertEquals(b, binding.getKey());
+            assertEquals("2 ElementsTest.java", binding.getSource());
+            binding.acceptTargetVisitor(new FailingTargetVisitor<T>() {
+              @Override public Void visitExposed(PrivateEnvironment privateEnvironment) {
+                assertEquals(ab, privateEnvironment.getExposedKeys());
+                return null;
+              }
+            });
+            return null;
+          }
         }
     );
   }