From d0ffb41a2e4bf9d0575090e49b84a28c47fc78ba Mon Sep 17 00:00:00 2001 From: Rahul Kamat Date: Tue, 21 Jan 2025 15:32:50 -0800 Subject: [PATCH] Implement first-half/second-half splitting of stage 2 optimizations. This change adds code to DefaultPassConfig for enabling splitting of stage 2 optimizations. I'm also adding a unit test to ensure that running the two halves of stage 2 result in the same output code as running all of stage 2 in one go. PiperOrigin-RevId: 718096864 --- .../javascript/jscomp/DefaultPassConfig.java | 530 +++++++++--------- .../google/javascript/jscomp/PassConfig.java | 5 +- .../javascript/jscomp/CompilerTest.java | 74 +++ 3 files changed, 356 insertions(+), 253 deletions(-) diff --git a/src/com/google/javascript/jscomp/DefaultPassConfig.java b/src/com/google/javascript/jscomp/DefaultPassConfig.java index 010e64cd37d..5b019eabf34 100644 --- a/src/com/google/javascript/jscomp/DefaultPassConfig.java +++ b/src/com/google/javascript/jscomp/DefaultPassConfig.java @@ -499,259 +499,19 @@ protected PassListBuilder getChecks() { @Override protected PassListBuilder getOptimizations(OptimizationPasses optimizationPasses) { PassListBuilder passes = new PassListBuilder(options); - // At this point all checks have been done. - if (options.exportTestFunctions) { - passes.maybeAdd(exportTestFunctions); - } - if (options.isPropertyRenamingOnlyCompilationMode()) { - passes.maybeAdd(removeUnnecessarySyntheticExterns); - TranspilationPasses.addTranspilationRuntimeLibraries(passes); - passes.maybeAdd(closureProvidesRequires); - passes.maybeAdd(processDefinesOptimize); - passes.maybeAdd(normalize); - TranspilationPasses.addTranspilationPasses(passes, options); - passes.maybeAdd(gatherExternPropertiesOptimize); - passes.maybeAdd(createEmptyPass(PassNames.BEFORE_STANDARD_OPTIMIZATIONS)); - passes.maybeAdd(inlineAndCollapseProperties); - passes.maybeAdd(closureOptimizePrimitives); - // If side-effects were protected, remove the protection now. - passes.maybeAdd(stripSideEffectProtection); - return passes; - } - - if (options.skipNonTranspilationPasses) { - // Reaching this if-condition means the 'getChecks()' phase has been skipped in favor of - // 'getTranspileOnlyPasses'. - return passes; - } - - if (options.getMergedPrecompiledLibraries()) { - // Weak sources aren't removed at the library level - passes.maybeAdd(removeWeakSources); - - // it would be safe to always recompute side effects even if not using precompiled libraries - // (the else case) but it's unnecessary so skip it to improve build times. - passes.maybeAdd(checkRegExpForOptimizations); - - // This runs during getChecks(), so only needs to be run here if using precompiled .typedasts - if (options.j2clPassMode.shouldAddJ2clPasses()) { - passes.maybeAdd(j2clSourceFileChecker); - } - } else { - addNonTypedAstNormalizationPasses(passes); - } - - // Remove synthetic extern declarations of names that are now defined in source - // This is expected to do nothing when in a monolithic build - passes.maybeAdd(removeUnnecessarySyntheticExterns); - - if (options.j2clPassMode.shouldAddJ2clPasses()) { - passes.maybeAdd(j2clPass); - } - - TranspilationPasses.addTranspilationRuntimeLibraries(passes); - - if (options.rewritePolyfills || options.getIsolatePolyfills()) { - TranspilationPasses.addRewritePolyfillPass(passes); - } - - passes.maybeAdd(injectRuntimeLibraries); - - if (options.closurePass) { - passes.maybeAdd(closureProvidesRequires); - } - - if (options.shouldRunReplaceMessagesForChrome()) { - passes.maybeAdd(replaceMessagesForChrome); - } else if (options.shouldRunReplaceMessagesPass()) { - if (options.doLateLocalization()) { - // With late localization we protect the messages from mangling by optimizations now, - // then actually replace them after optimizations. - // The purpose of doing this is to separate localization from optimization so we can - // optimize just once for all locales. - passes.maybeAdd(getProtectMessagesPass()); - } else { - // TODO(bradfordcsmith): At the moment we expect the optimized output may be slightly - // smaller if you replace messages before optimizing, but if we can change that, it would - // be good to drop this early replacement entirely. - passes.maybeAdd(getFullReplaceMessagesPass()); - } - } - - if (options.doLateLocalization()) { - passes.maybeAdd(protectLocaleData); - } - - // Replace 'goog.getCssName' before processing defines - if (options.closurePass && !options.shouldPreserveGoogLibraryPrimitives()) { - passes.maybeAdd(closureReplaceGetCssName); - } - - // Defines in code always need to be processed. - passes.maybeAdd(processDefinesOptimize); - passes.maybeAdd(createEmptyPass(PassNames.BEFORE_EARLY_OPTIMIZATIONS_TRANSPILATION)); - - passes.maybeAdd(normalize); - - // TODO(b/329447979): Add an early removeUnusedCode pass here - TranspilationPasses.addTranspilationPasses(passes, options); - - passes.maybeAdd(gatherGettersAndSetters); - - if (options.j2clPassMode.shouldAddJ2clPasses()) { - passes.maybeAdd(j2clUtilGetDefineRewriterPass); - } - - if (options.getInstrumentForCoverageOption() != InstrumentOption.NONE) { - passes.maybeAdd(instrumentForCodeCoverage); - } - - passes.maybeAdd(gatherExternPropertiesOptimize); - - passes.maybeAdd(createEmptyPass(PassNames.BEFORE_STANDARD_OPTIMIZATIONS)); - - // Abstract method removal works best on minimally modified code, and also - // only needs to run once. - if (options.closurePass && (options.removeAbstractMethods || options.removeClosureAsserts)) { - passes.maybeAdd(closureCodeRemoval); - } - - if (options.removeJ2clAsserts) { - passes.maybeAdd(j2clAssertRemovalPass); - } - - passes.maybeAdd(replaceToggles); - passes.maybeAdd(inlineAndCollapseProperties); - - if (options.getTweakProcessing().shouldStrip() - || !options.stripTypes.isEmpty() - || !options.stripNameSuffixes.isEmpty() - || !options.stripNamePrefixes.isEmpty()) { - passes.maybeAdd(stripCode); - } - - // Ideally this pass would run before transpilation which would allow it to be simplified. - // It needs to run after `inlineAndCollapseProperties` in order to identify idGenerator calls. - if (options.replaceIdGenerators) { - passes.maybeAdd(replaceIdGenerators); - } - - // Inline getters/setters in J2CL classes so that Object.defineProperties() calls (resulting - // from desugaring) don't block class stripping. - if (options.j2clPassMode.shouldAddJ2clPasses() - && options.getPropertyCollapseLevel() == PropertyCollapseLevel.ALL) { - // Relies on collapseProperties-triggered aggressive alias inlining. - passes.maybeAdd(j2clPropertyInlinerPass); - } - - if (options.inferConsts) { - passes.maybeAdd(inferConsts); - } - - // A marker pass to allow {@code ExtraPassConfig} passes to order themselves before - // RemoveUnusedCode. - passes.maybeAdd(createEmptyPass(PassNames.OBFUSCATION_PASS_MARKER)); - - // Running RemoveUnusedCode before disambiguate properties allows disambiguate properties to be - // more effective if code that would prevent disambiguation can be removed. - // TODO(b/66971163): Rename options since we're not actually using smartNameRemoval here now. - if (options.smartNameRemoval) { - - // These passes remove code that is dead because of define flags. - // If the dead code is weakly typed, running these passes before property - // disambiguation results in more code removal. - // The passes are one-time on purpose. (The later runs are loopable.) - if (options.foldConstants && (options.inlineVariables || options.inlineLocalVariables)) { - passes.maybeAdd(earlyInlineVariables); - passes.maybeAdd(earlyPeepholeOptimizations); - } - - passes.maybeAdd(removeUnusedCodeOnce); - } - - // Property disambiguation should only run once and needs to be done - // soon after type checking, both so that it can make use of type - // information and so that other passes can take advantage of the renamed - // properties. - if (options.shouldDisambiguateProperties() && options.isTypecheckingEnabled()) { - passes.maybeAdd(disambiguateProperties); - } - - if (options.computeFunctionSideEffects) { - passes.maybeAdd(markPureFunctions); - } - - passes.assertAllOneTimePasses(); - - if (options.smartNameRemoval) { - // Place one-time marker passes around this loop to prevent the addition of a looping pass - // above or below from accidentally becoming part of the loop. - passes.maybeAdd(createEmptyPass(PassNames.BEFORE_EARLY_OPTIMIZATION_LOOP)); - passes.addAll(getEarlyOptimizationLoopPasses()); - // TODO(): Remove this early loop or rename the option that enables it - // to something more appropriate. - passes.maybeAdd(createEmptyPass(PassNames.AFTER_EARLY_OPTIMIZATION_LOOP)); - } - - // This needs to come after the inline constants pass, which is run within - // the code removing passes. - if (options.closurePass) { - passes.maybeAdd(closureOptimizePrimitives); - } - - // ReplaceStrings runs after CollapseProperties in order to simplify - // pulling in values of constants defined in enums structures. It also runs - // after disambiguate properties and smart name removal so that it can - // correctly identify logging types and can replace references to string - // expressions. - if (!options.replaceStringsFunctionDescriptions.isEmpty()) { - passes.maybeAdd(replaceStrings); - } - - // TODO(user): This forces a first crack at crossChunkCodeMotion - // before devirtualization. Once certain functions are devirtualized, - // it confuses crossChunkCodeMotion ability to recognized that - // it is recursive. - - // TODO(user): This is meant for a temporary quick win. - // In the future, we might want to improve our analysis in - // CrossChunkCodeMotion so we don't need to do this. - if (options.shouldRunCrossChunkCodeMotion()) { - passes.maybeAdd(crossModuleCodeMotion); - } - - // Method devirtualization benefits from property disambiguation so - // it should run after that pass but before passes that do - // optimizations based on global names (like cross module code motion - // and inline functions). Smart Name Removal does better if run before - // this pass. - if (options.devirtualizeMethods) { - passes.maybeAdd(devirtualizeMethods); - } - - if (options.customPasses != null) { - passes.maybeAdd(getCustomPasses(CustomPassExecutionTime.BEFORE_OPTIMIZATION_LOOP)); - } - - passes.maybeAdd(createEmptyPass(PassNames.BEFORE_MAIN_OPTIMIZATIONS)); - - // Because FlowSensitiveInlineVariables does not operate on the global scope due to compilation - // time, we need to run it once before InlineFunctions so that we don't miss inlining - // opportunities when a function will be inlined into the global scope. - if (options.inlineVariables || options.inlineLocalVariables) { - passes.maybeAdd(flowSensitiveInlineVariables); - } - - passes.addAll(getMainOptimizationLoop()); - passes.maybeAdd(createEmptyPass(PassNames.AFTER_MAIN_OPTIMIZATIONS)); - // Some optimizations belong outside the loop because running them more - // than once would either have no benefit or be incorrect. - if (options.customPasses != null) { - passes.maybeAdd(getCustomPasses(CustomPassExecutionTime.AFTER_OPTIMIZATION_LOOP)); + switch (optimizationPasses) { + case FIRST_HALF: + passes.addAll(getEarlyOptimizationPasses()); + break; + case SECOND_HALF: + passes.addAll(getLateOptimizationPasses()); + break; + default: + passes.addAll(getEarlyOptimizationPasses()); + passes.addAll(getLateOptimizationPasses()); } - assertValidOrderForOptimizations(passes); return passes; } @@ -1061,6 +821,276 @@ private PassListBuilder getPostL10nOptimizations() { return loopPasses; } + /** + * These are the passes run in the first half of optimizations, which consists of transpilation + * and some early optimization passes. + */ + private PassListBuilder getEarlyOptimizationPasses() { + PassListBuilder passes = new PassListBuilder(options); + // At this point all checks have been done. + if (options.exportTestFunctions) { + passes.maybeAdd(exportTestFunctions); + } + if (options.isPropertyRenamingOnlyCompilationMode()) { + passes.maybeAdd(removeUnnecessarySyntheticExterns); + TranspilationPasses.addTranspilationRuntimeLibraries(passes); + passes.maybeAdd(closureProvidesRequires); + passes.maybeAdd(processDefinesOptimize); + passes.maybeAdd(normalize); + TranspilationPasses.addTranspilationPasses(passes, options); + passes.maybeAdd(gatherExternPropertiesOptimize); + passes.maybeAdd(createEmptyPass(PassNames.BEFORE_STANDARD_OPTIMIZATIONS)); + passes.maybeAdd(inlineAndCollapseProperties); + passes.maybeAdd(closureOptimizePrimitives); + // If side-effects were protected, remove the protection now. + passes.maybeAdd(stripSideEffectProtection); + return passes; + } + + if (options.skipNonTranspilationPasses) { + // Reaching this if-condition means the 'getChecks()' phase has been skipped in favor of + // 'getTranspileOnlyPasses'. + return passes; + } + + if (options.getMergedPrecompiledLibraries()) { + // Weak sources aren't removed at the library level + passes.maybeAdd(removeWeakSources); + + // it would be safe to always recompute side effects even if not using precompiled libraries + // (the else case) but it's unnecessary so skip it to improve build times. + passes.maybeAdd(checkRegExpForOptimizations); + + // This runs during getChecks(), so only needs to be run here if using precompiled .typedasts + if (options.j2clPassMode.shouldAddJ2clPasses()) { + passes.maybeAdd(j2clSourceFileChecker); + } + } else { + addNonTypedAstNormalizationPasses(passes); + } + + // Remove synthetic extern declarations of names that are now defined in source + // This is expected to do nothing when in a monolithic build + passes.maybeAdd(removeUnnecessarySyntheticExterns); + + if (options.j2clPassMode.shouldAddJ2clPasses()) { + passes.maybeAdd(j2clPass); + } + + TranspilationPasses.addTranspilationRuntimeLibraries(passes); + + if (options.rewritePolyfills || options.getIsolatePolyfills()) { + TranspilationPasses.addRewritePolyfillPass(passes); + } + + passes.maybeAdd(injectRuntimeLibraries); + + if (options.closurePass) { + passes.maybeAdd(closureProvidesRequires); + } + + if (options.shouldRunReplaceMessagesForChrome()) { + passes.maybeAdd(replaceMessagesForChrome); + } else if (options.shouldRunReplaceMessagesPass()) { + if (options.doLateLocalization()) { + // With late localization we protect the messages from mangling by optimizations now, + // then actually replace them after optimizations. + // The purpose of doing this is to separate localization from optimization so we can + // optimize just once for all locales. + passes.maybeAdd(getProtectMessagesPass()); + } else { + // TODO(bradfordcsmith): At the moment we expect the optimized output may be slightly + // smaller if you replace messages before optimizing, but if we can change that, it would + // be good to drop this early replacement entirely. + passes.maybeAdd(getFullReplaceMessagesPass()); + } + } + + if (options.doLateLocalization()) { + passes.maybeAdd(protectLocaleData); + } + + // Replace 'goog.getCssName' before processing defines + if (options.closurePass && !options.shouldPreserveGoogLibraryPrimitives()) { + passes.maybeAdd(closureReplaceGetCssName); + } + + // Defines in code always need to be processed. + passes.maybeAdd(processDefinesOptimize); + passes.maybeAdd(createEmptyPass(PassNames.BEFORE_EARLY_OPTIMIZATIONS_TRANSPILATION)); + + passes.maybeAdd(normalize); + + // TODO(b/329447979): Add an early removeUnusedCode pass here + TranspilationPasses.addTranspilationPasses(passes, options); + + passes.maybeAdd(gatherGettersAndSetters); + + if (options.j2clPassMode.shouldAddJ2clPasses()) { + passes.maybeAdd(j2clUtilGetDefineRewriterPass); + } + + if (options.getInstrumentForCoverageOption() != InstrumentOption.NONE) { + passes.maybeAdd(instrumentForCodeCoverage); + } + + passes.maybeAdd(gatherExternPropertiesOptimize); + + passes.maybeAdd(createEmptyPass(PassNames.BEFORE_STANDARD_OPTIMIZATIONS)); + + // Abstract method removal works best on minimally modified code, and also + // only needs to run once. + if (options.closurePass && (options.removeAbstractMethods || options.removeClosureAsserts)) { + passes.maybeAdd(closureCodeRemoval); + } + + if (options.removeJ2clAsserts) { + passes.maybeAdd(j2clAssertRemovalPass); + } + + passes.maybeAdd(replaceToggles); + passes.maybeAdd(inlineAndCollapseProperties); + + if (options.getTweakProcessing().shouldStrip() + || !options.stripTypes.isEmpty() + || !options.stripNameSuffixes.isEmpty() + || !options.stripNamePrefixes.isEmpty()) { + passes.maybeAdd(stripCode); + } + + // Ideally this pass would run before transpilation which would allow it to be simplified. + // It needs to run after `inlineAndCollapseProperties` in order to identify idGenerator calls. + if (options.replaceIdGenerators) { + passes.maybeAdd(replaceIdGenerators); + } + + // Inline getters/setters in J2CL classes so that Object.defineProperties() calls (resulting + // from desugaring) don't block class stripping. + if (options.j2clPassMode.shouldAddJ2clPasses() + && options.getPropertyCollapseLevel() == PropertyCollapseLevel.ALL) { + // Relies on collapseProperties-triggered aggressive alias inlining. + passes.maybeAdd(j2clPropertyInlinerPass); + } + + if (options.inferConsts) { + passes.maybeAdd(inferConsts); + } + + // A marker pass to allow {@code ExtraPassConfig} passes to order themselves before + // RemoveUnusedCode. + passes.maybeAdd(createEmptyPass(PassNames.OBFUSCATION_PASS_MARKER)); + + // Running RemoveUnusedCode before disambiguate properties allows disambiguate properties to be + // more effective if code that would prevent disambiguation can be removed. + // TODO(b/66971163): Rename options since we're not actually using smartNameRemoval here now. + if (options.smartNameRemoval) { + + // These passes remove code that is dead because of define flags. + // If the dead code is weakly typed, running these passes before property + // disambiguation results in more code removal. + // The passes are one-time on purpose. (The later runs are loopable.) + if (options.foldConstants && (options.inlineVariables || options.inlineLocalVariables)) { + passes.maybeAdd(earlyInlineVariables); + passes.maybeAdd(earlyPeepholeOptimizations); + } + + passes.maybeAdd(removeUnusedCodeOnce); + } + + // Property disambiguation should only run once and needs to be done + // soon after type checking, both so that it can make use of type + // information and so that other passes can take advantage of the renamed + // properties. + if (options.shouldDisambiguateProperties() && options.isTypecheckingEnabled()) { + passes.maybeAdd(disambiguateProperties); + } + + if (options.computeFunctionSideEffects) { + passes.maybeAdd(markPureFunctions); + } + + passes.assertAllOneTimePasses(); + return passes; + } + + /** + * These are the passes run in the second half of optimizations, which consists of the early + * optimization loop and the main optimization loop. + */ + private PassListBuilder getLateOptimizationPasses() { + PassListBuilder passes = new PassListBuilder(options); + if (options.smartNameRemoval) { + // Place one-time marker passes around this loop to prevent the addition of a looping pass + // above or below from accidentally becoming part of the loop. + passes.maybeAdd(createEmptyPass(PassNames.BEFORE_EARLY_OPTIMIZATION_LOOP)); + passes.addAll(getEarlyOptimizationLoopPasses()); + // TODO(): Remove this early loop or rename the option that enables it + // to something more appropriate. + passes.maybeAdd(createEmptyPass(PassNames.AFTER_EARLY_OPTIMIZATION_LOOP)); + } + + // This needs to come after the inline constants pass, which is run within + // the code removing passes. + if (options.closurePass) { + passes.maybeAdd(closureOptimizePrimitives); + } + + // ReplaceStrings runs after CollapseProperties in order to simplify + // pulling in values of constants defined in enums structures. It also runs + // after disambiguate properties and smart name removal so that it can + // correctly identify logging types and can replace references to string + // expressions. + if (!options.replaceStringsFunctionDescriptions.isEmpty()) { + passes.maybeAdd(replaceStrings); + } + + // TODO(user): This forces a first crack at crossChunkCodeMotion + // before devirtualization. Once certain functions are devirtualized, + // it confuses crossChunkCodeMotion ability to recognized that + // it is recursive. + + // TODO(user): This is meant for a temporary quick win. + // In the future, we might want to improve our analysis in + // CrossChunkCodeMotion so we don't need to do this. + if (options.shouldRunCrossChunkCodeMotion()) { + passes.maybeAdd(crossModuleCodeMotion); + } + + // Method devirtualization benefits from property disambiguation so + // it should run after that pass but before passes that do + // optimizations based on global names (like cross module code motion + // and inline functions). Smart Name Removal does better if run before + // this pass. + if (options.devirtualizeMethods) { + passes.maybeAdd(devirtualizeMethods); + } + + if (options.customPasses != null) { + passes.maybeAdd(getCustomPasses(CustomPassExecutionTime.BEFORE_OPTIMIZATION_LOOP)); + } + + passes.maybeAdd(createEmptyPass(PassNames.BEFORE_MAIN_OPTIMIZATIONS)); + + // Because FlowSensitiveInlineVariables does not operate on the global scope due to compilation + // time, we need to run it once before InlineFunctions so that we don't miss inlining + // opportunities when a function will be inlined into the global scope. + if (options.inlineVariables || options.inlineLocalVariables) { + passes.maybeAdd(flowSensitiveInlineVariables); + } + + passes.addAll(getMainOptimizationLoop()); + passes.maybeAdd(createEmptyPass(PassNames.AFTER_MAIN_OPTIMIZATIONS)); + + // Some optimizations belong outside the loop because running them more + // than once would either have no benefit or be incorrect. + if (options.customPasses != null) { + passes.maybeAdd(getCustomPasses(CustomPassExecutionTime.AFTER_OPTIMIZATION_LOOP)); + } + + assertValidOrderForOptimizations(passes); + return passes; + } + /** Creates the passes for the main optimization loop. */ private PassListBuilder getMainOptimizationLoop() { PassListBuilder passes = new PassListBuilder(options); diff --git a/src/com/google/javascript/jscomp/PassConfig.java b/src/com/google/javascript/jscomp/PassConfig.java index 80f21d3f24a..1360ed6733e 100644 --- a/src/com/google/javascript/jscomp/PassConfig.java +++ b/src/com/google/javascript/jscomp/PassConfig.java @@ -69,9 +69,8 @@ protected PassListBuilder getTranspileOnlyPasses() { */ public enum OptimizationPasses { ALL, - // TODO(user): Enable these in a follow-up CL. - // FIRST_HALF, // Passes from beginning of stage 2 until before the early optimization loop. - // SECOND_HALF, // Passes from the early optimization loop until the end of stage 2. + FIRST_HALF, // Passes from beginning of stage 2 until before the early optimization loop. + SECOND_HALF, // Passes from the early optimization loop until the end of stage 2. } /** diff --git a/test/com/google/javascript/jscomp/CompilerTest.java b/test/com/google/javascript/jscomp/CompilerTest.java index 4fe4331b6fd..ed91a8ae48d 100644 --- a/test/com/google/javascript/jscomp/CompilerTest.java +++ b/test/com/google/javascript/jscomp/CompilerTest.java @@ -3453,4 +3453,78 @@ public void testCreateSyntheticExternsInput_setsCorrectInputId() { assertThat(compiler.getInputsById().get(syntheticExterns.getInputId())) .isSameInstanceAs(syntheticExterns); } + + @Test + public void testStage2SplittingResultsInSameOutput() throws Exception { + Compiler compiler = new Compiler(new TestErrorManager()); + CompilerOptions options = new CompilerOptions(); + + options.setEmitUseStrict(false); + + CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options); + List externs = + Collections.singletonList( + SourceFile.fromCode( + "externs.js", + lines( + "var console = {};", // + " console.log = function() {};", + ""))); + List srcs = + Collections.singletonList( + SourceFile.fromCode( + "input.js", + lines( + "goog.module('foo');", + "const hello = 'hello';", + "function f() { return hello; }", + "console.log(f());"))); + compiler.init(externs, srcs, options); + + // This is what the output should look like after all optimizations. + String finalOutputAfterOptimizations = "console.log(\"hello\");"; + + // Stage 1 + compiler.parse(); + compiler.check(); + final byte[] stateAfterChecks = getSavedCompilerState(compiler); + + compiler = new Compiler(new TestErrorManager()); + compiler.init(externs, srcs, options); + restoreCompilerState(compiler, stateAfterChecks); + + // Stage 2, all passes + compiler.performTranspilationAndOptimizations(OptimizationPasses.ALL); + String source = compiler.toSource(); + assertThat(source).isEqualTo(finalOutputAfterOptimizations); // test output stage 2 code + + // Now reset the compiler and test splitting stage 2 into two halves. We want to test that the + // output is the same as when we run all of stage 2 in one go. + compiler = new Compiler(new TestErrorManager()); + compiler.init(externs, srcs, options); + + // Stage 1 + compiler.parse(); + compiler.check(); + final byte[] stateAfterChecks2 = getSavedCompilerState(compiler); + + compiler = new Compiler(new TestErrorManager()); + compiler.init(externs, srcs, options); + restoreCompilerState(compiler, stateAfterChecks2); + + // Stage 2, first half + compiler.performTranspilationAndOptimizations(OptimizationPasses.FIRST_HALF); + source = compiler.toSource(); + assertThat(source).isEqualTo("console.log(function(){return\"hello\"}());"); + + final byte[] stateAfterFirstHalfOptimizations = getSavedCompilerState(compiler); + compiler = new Compiler(new TestErrorManager()); + compiler.init(externs, srcs, options); + restoreCompilerState(compiler, stateAfterFirstHalfOptimizations); + + // Stage 2, second half + compiler.performTranspilationAndOptimizations(OptimizationPasses.SECOND_HALF); + source = compiler.toSource(); + assertThat(source).isEqualTo(finalOutputAfterOptimizations); // output is the same as before + } }