/*
 * Copyright 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.dev.javac;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.CompilerContext;
import com.google.gwt.dev.javac.JdtCompiler.UnitProcessor;
import com.google.gwt.dev.javac.typemodel.TypeOracle;
import com.google.gwt.dev.jjs.CorrelationFactory.DummyCorrelationFactory;
import com.google.gwt.dev.jjs.ast.JDeclaredType;
import com.google.gwt.dev.jjs.impl.GwtAstBuilder;
import com.google.gwt.dev.js.ast.JsRootScope;
import com.google.gwt.dev.resource.Resource;
import com.google.gwt.dev.util.StringInterner;
import com.google.gwt.dev.util.log.perf.SimpleEvent;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableList;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
import com.google.gwt.thirdparty.guava.common.collect.Interner;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import com.google.gwt.thirdparty.guava.common.hash.Funnels;
import com.google.gwt.thirdparty.guava.common.hash.Hasher;
import com.google.gwt.thirdparty.guava.common.hash.Hashing;
import com.google.gwt.thirdparty.guava.common.io.ByteStreams;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.ImportReference;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * Manages a centralized cache for compiled units.
 */
public class CompilationStateBuilder {

  /**
   * An opaque class that lets you compile more units later.
   */
  public class CompileMoreLater {

    private final class UnitProcessorImpl implements UnitProcessor {

      /**
       * A callback after the JDT compiler has compiled a .java file and created a matching
       * CompilationUnitDeclaration. We take this opportunity to create a matching CompilationUnit.
       */
      @Override
      public void process(CompilationUnitBuilder builder, CompilationUnitDeclaration cud,
          List<ImportReference> cudOriginaImports,
          List<CompiledClass> compiledClasses) {
        try (SimpleEvent ignored = new SimpleEvent("CSB process")) {
          // Collect parameter method names event when the compilation unit has errors.

          List<JDeclaredType> types = ImmutableList.of();
          final Set<String> jsniDeps = Sets.newHashSet();
          final Map<String, Binding> jsniRefs = Maps.newHashMap();
          Map<MethodDeclaration, JsniMethod> jsniMethods = ImmutableMap.of();
          List<String> apiRefs = ImmutableList.of();
          MethodArgNamesLookup methodArgs = new MethodArgNamesLookup();

          if (!cud.compilationResult().hasErrors()) {
            // Only collect jsniMethods, etc if the compilation unit does not have errors.
            jsniMethods =
                JsniMethodCollector.collectJsniMethods(cud, builder.getSourceMapPath(),
                    builder.getSource(), JsRootScope.INSTANCE, DummyCorrelationFactory.INSTANCE);

            JSORestrictionsChecker.check(jsoState, cud);

            // JSNI check + collect dependencies.
            JsniReferenceResolver
                .resolve(cud, cudOriginaImports, jsniMethods, jsniRefs,
                    new JsniReferenceResolver.TypeResolver() {
                      @Override
                      public ReferenceBinding resolveType(String sourceOrBinaryName) {
                        ReferenceBinding resolveType = compiler.resolveType(sourceOrBinaryName);
                        if (resolveType != null) {
                          jsniDeps.add(String.valueOf(resolveType.qualifiedSourceName()));
                        }
                        return resolveType;
                      }
                    });

            // GWT drives JDT in a way that allows missing references in the source to be
            // resolved to precompiled bytecode on disk (see INameEnvironment). This is
            // done so that annotations can be supplied in bytecode form only. But since no
            // AST is available for these types it creates the danger that some functional
            // class (not just an annotation) gets filled in but is missing AST. This would
            // cause later compiler stages to fail.
            BinaryTypeReferenceRestrictionsChecker.check(cud);

            if (!cud.compilationResult().hasErrors()) {
              // The above checks might have recorded errors; so we need to check here again.
              // So only construct the GWT AST if no JDT errors and no errors from our checks.
              types = GwtAstBuilder.process(cud, builder.getSourceMapPath(), jsniMethods, jsniRefs,
                  compilerContext);
            }

            // Only run this pass if JDT was able to compile the unit with no errors, otherwise
            // the JDT AST traversal might throw exceptions.
            methodArgs = MethodParamCollector.collect(cud, builder.getSourceMapPath());
          }
          apiRefs = compiler.collectApiRefs(cud);

          final Interner<String> interner = StringInterner.get();
          String packageName = interner.intern(Shared.getPackageName(builder.getTypeName()));

          List<String> unresolvedSimple = Lists.newArrayList();
          for (char[] simpleRef : cud.compilationResult().simpleNameReferences) {
            unresolvedSimple.add(interner.intern(String.valueOf(simpleRef)));
          }

          List<String> unresolvedQualified = Lists.newArrayList();
          for (char[][] qualifiedRef : cud.compilationResult().qualifiedReferences) {
            unresolvedQualified.add(interner.intern(CharOperation.toString(qualifiedRef)));
          }
          for (String jsniDep : jsniDeps) {
            unresolvedQualified.add(interner.intern(jsniDep));
          }
          for (int i = 0; i < apiRefs.size(); ++i) {
            apiRefs.set(i, interner.intern(apiRefs.get(i)));
          }

          // Dependencies need to be included even when the {@code cud} has errors as the unit might
          // be saved as a cached unit and its dependencies might be used to further invalidate
          // other units. See {@link
          // CompilationStateBuilder#removeInvalidCachedUnitsAndRescheduleCorrespondingBuilders}.
          Dependencies dependencies =
              new Dependencies(packageName, unresolvedQualified, unresolvedSimple, apiRefs);

          for (CompiledClass cc : compiledClasses) {
            allValidClasses.put(cc.getSourceName(), cc);
          }

          // Even when compilation units have errors, return a consistent builder.
          builder
              .setTypes(types)
              .setDependencies(dependencies)
              .setJsniMethods(jsniMethods.values())
              .setMethodArgs(methodArgs)
              .setClasses(compiledClasses)
              .setProblems(cud.compilationResult().getProblems());

          buildQueue.add(builder);
        }
      }
    }

    /**
     * A global cache of all currently-valid class files keyed by source name.
     * This is used to validate dependencies when reusing previously cached
     * units, to make sure they can be recompiled if necessary.
     */
    private final Map<String, CompiledClass> allValidClasses = Maps.newHashMap();

    private transient LinkedBlockingQueue<CompilationUnitBuilder> buildQueue;

    /**
     * The JDT compiler.
     */
    private final JdtCompiler compiler;

    /**
     * Continuation state for JSNI checking.
     */
    private final JSORestrictionsChecker.CheckerState jsoState =
        new JSORestrictionsChecker.CheckerState();

    private final boolean suppressErrors;

    private CompilerContext compilerContext;

    public CompileMoreLater(CompilerContext compilerContext) {
      this.compilerContext = compilerContext;
      this.compiler = new JdtCompiler(
          compilerContext, new UnitProcessorImpl());
      this.suppressErrors = !compilerContext.getOptions().isStrict();
    }

    /**
     * Compiles generated source files (unless cached) and adds them to the
     * CompilationState. If the compiler aborts, logs an error and throws
     * UnableToCompleteException.
     */
    public Collection<CompilationUnit> addGeneratedTypes(TreeLogger logger,
        Collection<GeneratedUnit> generatedUnits, CompilationState compilationState)
        throws UnableToCompleteException {
      try (SimpleEvent ignored = new SimpleEvent("CSB addGeneratedTypes")) {
        return doBuildGeneratedTypes(logger, compilerContext, generatedUnits, compilationState,
            this);
      }
    }

    public Map<String, CompiledClass> getValidClasses() {
      return Collections.unmodifiableMap(allValidClasses);
    }

    void addValidUnit(CompilationUnit unit) {
      compiler.addCompiledUnit(unit);
      for (CompiledClass cc : unit.getCompiledClasses()) {
        allValidClasses.put(cc.getSourceName(), cc);
      }
    }

    /**
     * Compiles the source code in each supplied CompilationUnitBuilder into a CompilationUnit and
     * reports errors.
     *
     * <p>A compilation unit is considered invalid if any of its dependencies (recursively) isn't
     * being compiled and isn't in allValidClasses, or if it has a signature that doesn't match
     * a dependency. Valid compilation units will be added to cachedUnits and the unit cache, and
     * their types will be added to allValidClasses. Invalid compilation units will be removed.</p>
     *
     * <p>I/O: serializes the AST of each Java type to DiskCache. (This happens even if the
     * compilation unit is later dropped.) If we're using the persistent unit cache, each valid
     * unit will also be serialized to the gwt-unitcache file. (As a result, each AST will be
     * copied there from the DiskCache.) A new persistent unit cache file will be created
     * each time compile() is called (if there's at least one valid unit) and the entire cache
     * will be rewritten to disk every {@link PersistentUnitCache#CACHE_FILE_THRESHOLD} files.</p>
     *
     * <p>This function won't report errors in invalid source files unless suppressErrors is false.
     * Instead, a summary giving the number of invalid files will be logged.</p>
     *
     * <p>If the JDT compiler aborts, logs an error and throws UnableToCompleteException. (This
     * doesn't happen for normal compile errors.)</p>
     */
    Collection<CompilationUnit> compile(TreeLogger logger, CompilerContext compilerContext,
        Collection<CompilationUnitBuilder> builders,
        Map<CompilationUnitBuilder, CompilationUnit> cachedUnits, String caller)
        throws UnableToCompleteException {
      UnitCache unitCache = compilerContext.getUnitCache();
      // Initialize the set of valid classes to the initially cached units.
      for (CompilationUnit unit : cachedUnits.values()) {
        for (CompiledClass cc : unit.getCompiledClasses()) {
          // Map by source name.
          String sourceName = cc.getSourceName();
          allValidClasses.put(sourceName, cc);
        }
      }

      List<CompilationUnit> resultUnits = Lists.newArrayList();
      do {
        final TreeLogger branch = logger.branch(TreeLogger.TRACE, "Compiling...");
        // Compile anything that needs to be compiled.
        buildQueue = new LinkedBlockingQueue<CompilationUnitBuilder>();
        final List<CompilationUnit> newlyBuiltUnits = Lists.newArrayList();
        final CompilationUnitBuilder sentinel = CompilationUnitBuilder.create((GeneratedUnit) null);
        final Throwable[] workerException = new Throwable[1];
        final ProgressLogger progressLogger =
            new ProgressLogger(branch, TreeLogger.TRACE, builders.size(), 10);
        Thread buildThread = new Thread() {
          @Override
          public void run() {
            int processedCompilationUnitBuilders = 0;
            try {
              do {
                CompilationUnitBuilder builder = buildQueue.take();
                if (!progressLogger.isTimerStarted()) {
                  // Set start time here, after first job has arrived, since it can take a little
                  // while for the first job to arrive, and this helps with the accuracy of the
                  // estimated times.
                  progressLogger.startTimer();
                }
                if (builder == sentinel) {
                  return;
                }
                // Expensive, must serialize GWT AST types to bytes.
                CompilationUnit unit = builder.build();
                newlyBuiltUnits.add(unit);

                processedCompilationUnitBuilders++;
                progressLogger.updateProgress(processedCompilationUnitBuilders);
              } while (true);
            } catch (Throwable e) {
              workerException[0] = e;
            }
          }
        };
        buildThread.setName("CompilationUnitBuilder");
        buildThread.start();
        long compilationStartNanos = System.nanoTime();
        try (SimpleEvent ignored = new SimpleEvent("JdtCompiler " + caller)) {
          compiler.doCompile(branch, builders);
        }
        buildQueue.add(sentinel);
        try {
          buildThread.join();
          long compilationNanos = System.nanoTime() - compilationStartNanos;
          // Convert nanos to seconds.
          double compilationSeconds = compilationNanos / (double) TimeUnit.SECONDS.toNanos(1);
          branch.log(TreeLogger.TRACE,
              String.format("Compilation completed in %.02f seconds", compilationSeconds));
          if (workerException[0] != null) {
            throw workerException[0];
          }
        } catch (RuntimeException e) {
          throw e;
        } catch (Throwable e) {
          throw new RuntimeException("Exception processing units", e);
        } finally {
          buildQueue = null;
        }
        resultUnits.addAll(newlyBuiltUnits);
        builders.clear();

        // Resolve all newly built unit deps against the global classes.
        for (CompilationUnit unit : newlyBuiltUnits) {
          unit.getDependencies().resolve(allValidClasses);
        }

        removeInvalidCachedUnitsAndRescheduleCorrespondingBuilders(logger, builders, cachedUnits);
      } while (builders.size() > 0);

      for (CompilationUnit unit : resultUnits) {
        unitCache.add(unit);
      }

      // Any remaining cached units are valid now.
      resultUnits.addAll(cachedUnits.values());

      // Done with a pass of the build - tell the cache its OK to cleanup
      // stale cache files.
      unitCache.cleanup(logger);

      // Report warnings.
      Type logLevelForWarnings = suppressErrors ? TreeLogger.DEBUG : TreeLogger.WARN;
      int warningCount =
          CompilationProblemReporter.logWarnings(logger, logLevelForWarnings, resultUnits);
      if (warningCount > 0 && !logger.isLoggable(logLevelForWarnings)) {
        logger.log(TreeLogger.INFO, "Ignored " + warningCount + " unit"
            + (warningCount > 1 ? "s" : "") + " with compilation errors in first pass.\n"
            + "Compile with -strict or with -logLevel set to DEBUG or WARN to see all errors.");
      }

      // Index errors so that error chains can be reported.
      CompilationProblemReporter.indexErrors(compilerContext.getCompilationErrorsIndex(),
          resultUnits);

      // Report error chains and hints.
      Type logLevelForErrors = suppressErrors ? TreeLogger.TRACE : TreeLogger.ERROR;
      int errorCount = CompilationProblemReporter.logErrorTrace(logger, logLevelForErrors,
          compilerContext, resultUnits, false);
      if (errorCount > 0 && !logger.isLoggable(logLevelForErrors)
          && logger.isLoggable(TreeLogger.INFO)) {
        logger.log(TreeLogger.INFO, "Ignored " + errorCount + " unit" + (errorCount > 1 ? "s" : "")
            + " with compilation errors in first pass.\n"
            + "Compile with -strict or with -logLevel set to TRACE or DEBUG to see all errors.");
      }

      // Sort units to ensure stable output.
      Collections.sort(resultUnits, CompilationUnit.COMPARATOR);

      return resultUnits;
    }

    /**
     * Removes cached units that fail validation with the current set of valid classes; also
     * add the builder of the invalidated unit back for retry later.
     */
    private void removeInvalidCachedUnitsAndRescheduleCorrespondingBuilders(TreeLogger logger,
        Collection<CompilationUnitBuilder> builders,
        Map<CompilationUnitBuilder, CompilationUnit> cachedUnits) {

      /*
       * Invalidate any cached units with invalid refs.
       */
      Collection<CompilationUnit> invalidatedUnits = Lists.newArrayList();
      for (Iterator<Entry<CompilationUnitBuilder, CompilationUnit>> it =
          cachedUnits.entrySet().iterator(); it.hasNext();) {
        Entry<CompilationUnitBuilder, CompilationUnit> entry = it.next();
        CompilationUnit unit = entry.getValue();
        boolean isValid = unit.getDependencies().validate(logger, allValidClasses);
        if (isValid && unit.isError()) {
          // See if the unit has classes that can't provide a
          // NameEnvironmentAnswer
          for (CompiledClass cc : unit.getCompiledClasses()) {
            try {
              cc.getNameEnvironmentAnswer();
            } catch (ClassFormatException ex) {
              isValid = false;
              break;
            }
          }
        }
        if (!isValid) {
          if (logger.isLoggable(TreeLogger.TRACE)) {
            logger.log(TreeLogger.TRACE, "Invalid Unit: " + unit.getTypeName());
          }
          invalidatedUnits.add(unit);
          builders.add(entry.getKey());
          it.remove();
        }
      }

      if (invalidatedUnits.size() > 0) {
        if (logger.isLoggable(TreeLogger.TRACE)) {
          logger.log(TreeLogger.TRACE, "Invalid units found: " + invalidatedUnits.size());
        }
      }

      // Any units we invalidated must now be removed from the valid classes.
      for (CompilationUnit unit : invalidatedUnits) {
        for (CompiledClass cc : unit.getCompiledClasses()) {
          allValidClasses.remove(cc.getSourceName());
        }
      }
    }
  }

  private static final CompilationStateBuilder instance = new CompilationStateBuilder();

  /**
   * Compiles the given source files and adds them to the CompilationState. See
   * {@link CompileMoreLater#compile} for details.
   *
   * @throws UnableToCompleteException if the compiler aborts (not a normal compile error).
   */
  public static CompilationState buildFrom(TreeLogger logger, CompilerContext compilerContext,
      Set<Resource> resources)
      throws UnableToCompleteException {
    try (SimpleEvent ignored = new SimpleEvent("CSB build from oracle")) {
      return instance.doBuildFrom(logger, compilerContext, resources);
    }
  }

  /**
   * Build a new compilation state from a source oracle. Allow the caller to
   * specify a compiler delegate that will handle undefined names.
   *
   * TODO: maybe use a finer brush than to synchronize the whole thing.
   */
  public synchronized CompilationState doBuildFrom(TreeLogger logger,
      CompilerContext compilerContext, Set<Resource> resources)
    throws UnableToCompleteException {
    UnitCache unitCache = compilerContext.getUnitCache();
    assert unitCache != null : "CompilerContext should always contain a unit cache.";

    // Units we definitely want to build.
    List<CompilationUnitBuilder> builders = Lists.newArrayList();

    // Units we don't want to rebuild unless we have to.
    Map<CompilationUnitBuilder, CompilationUnit> cachedUnits = Maps.newIdentityHashMap();

    CompileMoreLater compileMoreLater = new CompileMoreLater(compilerContext);

    // For each incoming Java source file...
    for (Resource resource : resources) {
      // Create a builder for all incoming units.
      CompilationUnitBuilder builder = CompilationUnitBuilder.create(resource);

      CompilationUnit cachedUnit = unitCache.find(resource.getPathPrefix() + resource.getPath());

      if (cachedUnit != null && cachedUnit.getLastModified() == resource.getLastModified()) {
        // As verification is costly, this only runs the check when assertions are enabled.
        assert verifyContentId(logger, resource, cachedUnit);
      }

      // Try to rescue cached units from previous sessions where a jar has been
      // recompiled.
      if (cachedUnit != null && cachedUnit.getLastModified() != resource.getLastModified()) {
        unitCache.remove(cachedUnit);
        if (cachedUnit instanceof CachedCompilationUnit
            && cachedUnit.getContentId().equals(builder.getContentId())) {
          CachedCompilationUnit updatedUnit =
              new CachedCompilationUnit((CachedCompilationUnit) cachedUnit, resource
                  .getLastModified(), resource.getLocation());
          unitCache.add(updatedUnit);
        } else {
          cachedUnit = null;
        }
      }
      if (cachedUnit != null) {
        cachedUnits.put(builder, cachedUnit);
        compileMoreLater.addValidUnit(cachedUnit);
        continue;
      }
      builders.add(builder);
      compilerContext.getMinimalRebuildCache().addSourceCompilationUnitName(
          builder.getTypeName());
    }
    int cachedSourceCount = cachedUnits.size();
    int sourceCount = resources.size();
    if (logger.isLoggable(TreeLogger.TRACE)) {
      logger.log(TreeLogger.TRACE, "Found " + cachedSourceCount + " cached/archived units.  Used "
          + cachedSourceCount + " / " + sourceCount + " units from cache.");
    }

    Collection<CompilationUnit> resultUnits = compileMoreLater.compile(
        logger, compilerContext, builders, cachedUnits,
        "Source Oracle");

    TypeOracle typeOracle = new TypeOracle();
    CompilationUnitTypeOracleUpdater typeOracleUpdater =
        new CompilationUnitTypeOracleUpdater(typeOracle);

    CompilationState compilationState = new CompilationState(logger, compilerContext, typeOracle,
        typeOracleUpdater, resultUnits, compileMoreLater);
    compilationState.incrementStaticSourceCount(sourceCount);
    compilationState.incrementCachedStaticSourceCount(cachedSourceCount);
    return compilationState;
  }

  private boolean verifyContentId(TreeLogger logger, Resource resource, CompilationUnit cachedUnit) {
    if (!cachedUnit.getContentId().equals(getResourceContentId(resource))) {
      logger.log(TreeLogger.WARN, "Modification date hasn't changed but contentId has changed for "
          + resource.getLocation());
    }
    return true;
  }

  private ContentId getResourceContentId(Resource resource) {
    try (InputStream in = resource.openContents()) {
      /*
       * In most cases openContents() will throw an exception, however in the case of a
       * ZipFileResource it might return null causing an NPE in Util.copyNoClose(), see issue 4359.
       */
      if (in == null) {
        throw new RuntimeException("Unexpected error reading resource '" + resource + "'");
      }
      Hasher hasher = Hashing.murmur3_128().newHasher();
      ByteStreams.copy(in, Funnels.asOutputStream(hasher));
      String hash = hasher.hash().toString().toUpperCase(Locale.ROOT);
      return new ContentId(Shared.getTypeName(resource), hash);
    } catch (IOException e) {
      throw new RuntimeException("Unexpected error reading resource '" + resource + "'", e);
    }
  }

  /**
   * Compile new generated units into an existing state.
   *
   * TODO: maybe use a finer brush than to synchronize the whole thing.
   */
  synchronized Collection<CompilationUnit> doBuildGeneratedTypes(TreeLogger logger,
      CompilerContext compilerContext, Collection<GeneratedUnit> generatedUnits,
      CompilationState compilationState, CompileMoreLater compileMoreLater)
      throws UnableToCompleteException {
    UnitCache unitCache = compilerContext.getUnitCache();

    // Units we definitely want to build.
    List<CompilationUnitBuilder> builders = Lists.newArrayList();
    // Units we don't want to rebuild unless we have to.
    Map<CompilationUnitBuilder, CompilationUnit> cachedUnits = Maps.newIdentityHashMap();

    // For each incoming generated Java source file...
    for (GeneratedUnit generatedUnit : generatedUnits) {
      // Create a builder for all incoming units.
      CompilationUnitBuilder builder = CompilationUnitBuilder.create(generatedUnit);

      // Look for units previously compiled
      CompilationUnit cachedUnit = unitCache.find(builder.getContentId());
      // Keep track of the names of units that are new or are known to have changed.
      if (cachedUnit != null) {
        // Recompile generated units with errors so source can be dumped.
        if (!cachedUnit.isError()) {
          cachedUnits.put(builder, cachedUnit);
          compileMoreLater.addValidUnit(cachedUnit);
          continue;
        }
      }
      builders.add(builder);
      compilerContext.getMinimalRebuildCache().addSourceCompilationUnitName(
          builder.getTypeName());
    }
    if (compilerContext.getOptions().isIncrementalCompileEnabled()) {
      compilerContext.getMinimalRebuildCache().recordGeneratedUnits(generatedUnits);
    }
    compilationState.incrementGeneratedSourceCount(builders.size() + cachedUnits.size());
    compilationState.incrementCachedGeneratedSourceCount(cachedUnits.size());
    return compileMoreLater.compile(logger, compilerContext, builders,
        cachedUnits, "CSB Generated");
  }
}
