When compiled delta is rejected, reset incremental compiler back to last known good state(last accepted state).

This is needed so that compileExpression requests are serviced from that accepted state.

Bug: https://github.com/dart-lang/sdk/issues/34025
Change-Id: I592d2af50e59a721e1feb1699c6d56bcd568465f
Reviewed-on: https://dart-review.googlesource.com/72540
Commit-Queue: Alexander Aprelev <aam@google.com>
Reviewed-by: Jens Johansen <jensj@google.com>
diff --git a/pkg/front_end/lib/src/api_prototype/incremental_kernel_generator.dart b/pkg/front_end/lib/src/api_prototype/incremental_kernel_generator.dart
index 8d4e803..eef20ee 100644
--- a/pkg/front_end/lib/src/api_prototype/incremental_kernel_generator.dart
+++ b/pkg/front_end/lib/src/api_prototype/incremental_kernel_generator.dart
@@ -25,6 +25,13 @@
         initializeFromDillUri);
   }
 
+  factory IncrementalKernelGenerator.fromComponent(
+      CompilerOptions options, Uri entryPoint, Component component) {
+    return new IncrementalCompiler.fromComponent(
+        new CompilerContext(new ProcessedOptions(options, [entryPoint])),
+        component);
+  }
+
   /// Returns a component whose libraries are the recompiled libraries,
   /// or - in the case of [fullComponent] - a full Component.
   Future<Component> computeDelta({Uri entryPoint, bool fullComponent});
diff --git a/pkg/front_end/lib/src/fasta/incremental_compiler.dart b/pkg/front_end/lib/src/fasta/incremental_compiler.dart
index 0697cc1..f9a4ca8 100644
--- a/pkg/front_end/lib/src/fasta/incremental_compiler.dart
+++ b/pkg/front_end/lib/src/fasta/incremental_compiler.dart
@@ -86,13 +86,20 @@
   List<LibraryBuilder> platformBuilders;
   Map<Uri, LibraryBuilder> userBuilders;
   final Uri initializeFromDillUri;
+  Component componentToInitializeFrom;
   bool initializedFromDill = false;
   bool hasToCheckPackageUris = false;
 
   KernelIncrementalTarget userCode;
 
+  IncrementalCompiler.fromComponent(
+      this.context, Component this.componentToInitializeFrom)
+      : ticker = context.options.ticker,
+        initializeFromDillUri = null;
+
   IncrementalCompiler(this.context, [this.initializeFromDillUri])
-      : ticker = context.options.ticker;
+      : ticker = context.options.ticker,
+        componentToInitializeFrom = null;
 
   @override
   Future<Component> computeDelta(
@@ -142,6 +149,8 @@
                   null);
             }
           }
+        } else if (componentToInitializeFrom != null) {
+          initializeFromComponent(summaryBytes, uriTranslator, c, data);
         }
         appendLibraries(data, bytesLength);
 
@@ -171,9 +180,19 @@
       }
 
       Set<Uri> invalidatedUris = this.invalidatedUris.toSet();
-      if (data.includeUserLoadedLibraries || fullComponent) {
+      if ((data.includeUserLoadedLibraries &&
+              componentToInitializeFrom ==
+                  null) // when loading state from component no need to invalidate anything
+          ||
+          fullComponent) {
         invalidatedUris.add(entryPoint);
       }
+      if (componentToInitializeFrom != null) {
+        // Once compiler was initialized from component, no need to skip logic
+        // above that adds entryPoint to invalidatedUris for successive
+        // [computeDelta] calls.
+        componentToInitializeFrom = null;
+      }
 
       ClassHierarchy hierarchy = userCode?.loader?.hierarchy;
       Set<LibraryBuilder> notReusedLibraries;
@@ -414,6 +433,43 @@
     return bytesLength;
   }
 
+  // This procedure will set up compiler from [componentToInitializeFrom].
+  void initializeFromComponent(
+      List<int> summaryBytes,
+      UriTranslator uriTranslator,
+      CompilerContext c,
+      IncrementalCompilerData data) {
+    ticker.logMs("Read initializeFromComponent");
+
+    // [libraries] and [uriToSource] from [componentToInitializeFrom] take
+    // precedence over what was already read into [data.component]. Assumption
+    // is that [data.component] is initialized with standard prebuilt various
+    // platform libraries.
+    List<Library> combinedLibs = <Library>[];
+    Set<Uri> readLibs =
+        componentToInitializeFrom.libraries.map((lib) => lib.fileUri).toSet();
+    combinedLibs.addAll(componentToInitializeFrom.libraries);
+    for (Library lib in data.component.libraries) {
+      if (!readLibs.contains(lib.fileUri)) {
+        combinedLibs.add(lib);
+      }
+    }
+    Map<Uri, Source> combinedMaps = new Map<Uri, Source>();
+    combinedMaps.addAll(componentToInitializeFrom.uriToSource);
+    Set<Uri> uris = combinedMaps.keys.toSet();
+    for (MapEntry<Uri, Source> entry in data.component.uriToSource.entries) {
+      if (!uris.contains(entry.key)) {
+        combinedMaps[entry.key] = entry.value;
+      }
+    }
+
+    data.component =
+        new Component(libraries: combinedLibs, uriToSource: combinedMaps)
+          ..mainMethod = componentToInitializeFrom.mainMethod;
+    data.userLoadedUriMain = data.component.mainMethod;
+    data.includeUserLoadedLibraries = true;
+  }
+
   void appendLibraries(IncrementalCompilerData data, int bytesLength) {
     if (data.component != null) {
       dillLoadedData.loader
diff --git a/pkg/vm/lib/frontend_server.dart b/pkg/vm/lib/frontend_server.dart
index b63071f..6397f17 100644
--- a/pkg/vm/lib/frontend_server.dart
+++ b/pkg/vm/lib/frontend_server.dart
@@ -159,6 +159,10 @@
   /// won't recompile sources that were previously reported as changed.
   void acceptLastDelta();
 
+  /// Rejects results of previous compilation and sets compiler back to last
+  /// accepted state.
+  Future<void> rejectLastDelta();
+
   /// This let's compiler know that source file identifed by `uri` was changed.
   void invalidate(Uri uri);
 
@@ -494,6 +498,14 @@
   }
 
   @override
+  Future<void> rejectLastDelta() async {
+    await _generator.reject();
+    final String boundaryKey = new Uuid().generateV4();
+    _outputStream.writeln('result $boundaryKey');
+    _outputStream.writeln(boundaryKey);
+  }
+
+  @override
   void invalidate(Uri uri) {
     _generator.invalidate(uri);
   }
@@ -626,6 +638,8 @@
           state = _State.COMPILE_EXPRESSION_EXPRESSION;
         } else if (string == 'accept') {
           compiler.acceptLastDelta();
+        } else if (string == 'reject') {
+          await compiler.rejectLastDelta();
         } else if (string == 'reset') {
           compiler.resetIncrementalCompiler();
         } else if (string == 'quit') {
diff --git a/pkg/vm/lib/incremental_compiler.dart b/pkg/vm/lib/incremental_compiler.dart
index 8238343..4cfedcd 100644
--- a/pkg/vm/lib/incremental_compiler.dart
+++ b/pkg/vm/lib/incremental_compiler.dart
@@ -17,16 +17,26 @@
 /// accepted.
 class IncrementalCompiler {
   IncrementalKernelGenerator _generator;
+
+  // Component that reflect current state of the compiler, which has not
+  // been yet accepted by the client. Is [null] if no compilation was done
+  // since last accept/reject acknowledgement by the client.
+  Component _candidate;
+  // Component that reflect the state that was most recently accepted by the
+  // client. Is [null], if no compilation results were accepted by the client.
+  Component _lastKnownGood;
+
   List<Component> _pendingDeltas;
   CompilerOptions _compilerOptions;
   bool initialized = false;
   bool fullComponent = false;
   Uri initializeFromDillUri;
+  Uri _entryPoint;
 
-  IncrementalCompiler(this._compilerOptions, Uri entryPoint,
+  IncrementalCompiler(this._compilerOptions, this._entryPoint,
       {this.initializeFromDillUri}) {
     _generator = new IncrementalKernelGenerator(
-        _compilerOptions, entryPoint, initializeFromDillUri);
+        _compilerOptions, _entryPoint, initializeFromDillUri);
     _pendingDeltas = <Component>[];
   }
 
@@ -41,25 +51,24 @@
     fullComponent = false;
     final bool firstDelta = _pendingDeltas.isEmpty;
     _pendingDeltas.add(component);
-    if (firstDelta) {
-      return component;
-    }
-
-    // If more than one delta is pending, we need to combine them.
-    Procedure mainMethod;
-    Map<Uri, Library> combined = <Uri, Library>{};
-    for (Component delta in _pendingDeltas) {
-      if (delta.mainMethod != null) {
-        mainMethod = delta.mainMethod;
+    if (!firstDelta) {
+      // If more than one delta is pending, we need to combine them.
+      Procedure mainMethod;
+      Map<Uri, Library> combined = <Uri, Library>{};
+      for (Component delta in _pendingDeltas) {
+        if (delta.mainMethod != null) {
+          mainMethod = delta.mainMethod;
+        }
+        for (Library library in delta.libraries) {
+          combined[library.importUri] = library;
+        }
       }
-      for (Library library in delta.libraries) {
-        combined[library.importUri] = library;
-      }
+      // TODO(vegorov) this needs to merge metadata repositories from deltas.
+      component = new Component(libraries: combined.values.toList())
+        ..mainMethod = mainMethod;
     }
-
-    // TODO(vegorov) this needs to merge metadata repositories from deltas.
-    return new Component(libraries: combined.values.toList())
-      ..mainMethod = mainMethod;
+    _candidate = component;
+    return component;
   }
 
   /// This lets incremental compiler know that results of last [compile] call
@@ -67,6 +76,40 @@
   /// results.
   accept() {
     _pendingDeltas.clear();
+
+    Map<Uri, Library> combined = <Uri, Library>{};
+    if (_lastKnownGood != null) {
+      // TODO(aam): Figure out how to skip no-longer-used libraries from
+      // [_lastKnownGood] libraries.
+      for (Library library in _lastKnownGood.libraries) {
+        combined[library.importUri] = library;
+      }
+    }
+    for (Library library in _candidate.libraries) {
+      combined[library.importUri] = library;
+    }
+    _lastKnownGood = new Component(
+      libraries: combined.values.toList(),
+      uriToSource: _candidate.uriToSource,
+    )..mainMethod = _candidate.mainMethod;
+    for (final repo in _candidate.metadata.values) {
+      _lastKnownGood.addMetadataRepository(repo);
+    }
+
+    _candidate = null;
+  }
+
+  /// This lets incremental compiler know that results of last [compile] call
+  /// were rejected. Subsequent [compile] or [compileExpression] calls need to
+  /// be processed without changes picked up by rejected [compile] call.
+  reject() async {
+    _pendingDeltas.clear();
+    _candidate = null;
+    // Need to reset and warm up compiler so that expression evaluation requests
+    // are processed in that known good state.
+    _generator = new IncrementalKernelGenerator.fromComponent(
+        _compilerOptions, _entryPoint, _lastKnownGood);
+    await _generator.computeDelta(entryPoint: _entryPoint);
   }
 
   /// This tells incremental compiler that it needs rescan [uri] file during
diff --git a/pkg/vm/test/frontend_server_test.dart b/pkg/vm/test/frontend_server_test.dart
index 89dfafa..4ca4370 100644
--- a/pkg/vm/test/frontend_server_test.dart
+++ b/pkg/vm/test/frontend_server_test.dart
@@ -589,6 +589,137 @@
       expect(await result, 0);
     });
 
+    test('compile expression when delta is rejected', () async {
+      var fileLib = new File('${tempDir.path}/lib.dart')..createSync();
+      fileLib.writeAsStringSync("foo() => 42;\n");
+      var file = new File('${tempDir.path}/foo.dart')..createSync();
+      file.writeAsStringSync("import 'lib.dart'; main1() => print(foo);\n");
+      var dillFile = new File('${tempDir.path}/app.dill');
+      expect(dillFile.existsSync(), equals(false));
+      final List<String> args = <String>[
+        '--sdk-root=${sdkRoot.toFilePath()}',
+        '--strong',
+        '--incremental',
+        '--platform=${platformKernel.path}',
+        '--output-dill=${dillFile.path}'
+      ];
+
+      final StreamController<List<int>> inputStreamController =
+          new StreamController<List<int>>();
+      final StreamController<List<int>> stdoutStreamController =
+          new StreamController<List<int>>();
+      final IOSink ioSink = new IOSink(stdoutStreamController.sink);
+      StreamController<String> receivedResults = new StreamController<String>();
+
+      String boundaryKey;
+      stdoutStreamController.stream
+          .transform(utf8.decoder)
+          .transform(const LineSplitter())
+          .listen((String s) {
+        print(s);
+        const String RESULT_OUTPUT_SPACE = 'result ';
+        if (boundaryKey == null) {
+          if (s.startsWith(RESULT_OUTPUT_SPACE)) {
+            boundaryKey = s.substring(RESULT_OUTPUT_SPACE.length);
+          }
+        } else {
+          if (s.startsWith(boundaryKey)) {
+            receivedResults.add(s.length > boundaryKey.length
+                ? s.substring(boundaryKey.length + 1)
+                : null);
+            boundaryKey = null;
+          }
+        }
+      });
+
+      final Future<int> result =
+          starter(args, input: inputStreamController.stream, output: ioSink);
+      inputStreamController.add('compile ${file.path}\n'.codeUnits);
+      int count = 0;
+      receivedResults.stream.listen((String outputFilenameAndErrorCount) {
+        if (count == 0) {
+          // First request was to 'compile', which resulted in full kernel file.
+          CompilationResult result =
+              new CompilationResult.parse(outputFilenameAndErrorCount);
+
+          expect(dillFile.existsSync(), equals(true));
+          expect(result.filename, dillFile.path);
+          expect(result.errorsCount, equals(0));
+          inputStreamController.add('accept\n'.codeUnits);
+
+          // 'compile-expression <boundarykey>
+          // expression
+          // definitions (one per line)
+          // ...
+          // <boundarykey>
+          // type-defintions (one per line)
+          // ...
+          // <boundarykey>
+          // <libraryUri: String>
+          // <klass: String>
+          // <isStatic: true|false>
+          inputStreamController.add('''
+compile-expression abc
+main1
+abc
+abc
+${file.uri}
+
+true
+'''
+              .codeUnits);
+          count += 1;
+        } else if (count == 1) {
+          // Second request was to 'compile-expression', which resulted in
+          // kernel file with a function that wraps compiled expression.
+          expect(outputFilenameAndErrorCount, isNotNull);
+          CompilationResult result =
+              new CompilationResult.parse(outputFilenameAndErrorCount);
+          print(outputFilenameAndErrorCount);
+
+          expect(result.errorsCount, equals(0));
+          File outputFile = new File(result.filename);
+          expect(outputFile.existsSync(), equals(true));
+          expect(outputFile.lengthSync(), isPositive);
+
+          file.writeAsStringSync("import 'lib.dart'; main() => foo();\n");
+          inputStreamController.add('recompile ${file.path} abc\n'
+              '${file.path}\n'
+              'abc\n'
+              .codeUnits);
+
+          count += 1;
+        } else if (count == 2) {
+          // Third request was to recompile the script after renaming a function.
+          expect(outputFilenameAndErrorCount, isNotNull);
+          CompilationResult result =
+              new CompilationResult.parse(outputFilenameAndErrorCount);
+          expect(result.errorsCount, equals(0));
+
+          inputStreamController.add('reject\n'.codeUnits);
+          count += 1;
+        } else if (count == 3) {
+          // Fourth request was to reject the compilation results.
+          inputStreamController.add(
+              'compile-expression abc\nmain1\nabc\nabc\n${file.uri}\n\ntrue\n'
+                  .codeUnits);
+          count += 1;
+        } else {
+          expect(count, 4);
+          // Fifth request was to 'compile-expression' that references original
+          // function, which should still be successful.
+          expect(outputFilenameAndErrorCount, isNotNull);
+          CompilationResult result =
+              new CompilationResult.parse(outputFilenameAndErrorCount);
+          expect(result.errorsCount, equals(0));
+          inputStreamController.add('quit\n'.codeUnits);
+        }
+      });
+
+      expect(await result, 0);
+      inputStreamController.close();
+    }, timeout: Timeout.factor(100));
+
     test('recompile request keeps incremental output dill filename', () async {
       var file = new File('${tempDir.path}/foo.dart')..createSync();
       file.writeAsStringSync("main() {}\n");
@@ -630,7 +761,6 @@
           starter(args, input: inputStreamController.stream, output: ioSink);
       inputStreamController.add('compile ${file.path}\n'.codeUnits);
       int count = 0;
-      Completer<bool> allDone = new Completer<bool>();
       receivedResults.stream.listen((String outputFilenameAndErrorCount) {
         CompilationResult result =
             new CompilationResult.parse(outputFilenameAndErrorCount);
@@ -655,11 +785,9 @@
           expect(result.filename, dillIncFile.path);
           expect(result.errorsCount, 0);
           expect(dillIncFile.existsSync(), equals(true));
-          allDone.complete(true);
+          inputStreamController.add('quit\n'.codeUnits);
         }
       });
-      expect(await allDone.future, true);
-      inputStreamController.add('quit\n'.codeUnits);
       expect(await result, 0);
       inputStreamController.close();
     });
@@ -815,7 +943,7 @@
         '--sdk-root=${sdkRoot.toFilePath()}',
         '--strong',
         '--incremental',
-        '--platform=$platformKernel',
+        '--platform=${platformKernel.path}',
         '--output-dill=${dillFile.path}',
         '--output-incremental-dill=${incrementalDillFile.path}'
       ];
@@ -901,8 +1029,7 @@
             }
 
             // Include platform and verify.
-            component =
-                loadComponentFromBinary(platformKernel.toFilePath(), component);
+            component = loadComponentFromBinary(platformKernel.path, component);
             expect(component.mainMethod, isNotNull);
             verifyComponent(component);
 
@@ -927,8 +1054,7 @@
                 reason: "Expect the same number of sources after a reset.");
 
             // Include platform and verify.
-            component =
-                loadComponentFromBinary(platformKernel.toFilePath(), component);
+            component = loadComponentFromBinary(platformKernel.path, component);
             expect(component.mainMethod, isNotNull);
             verifyComponent(component);
 
@@ -953,11 +1079,10 @@
 
             // Reload with 1 change
             inputStreamController.add('accept\n'.codeUnits);
-            inputStreamController
-                .add('recompile ${dart2jsOtherFile.path} x$count\n'
-                    '${dart2jsOtherFile.uri}\n'
-                    'x$count\n'
-                    .codeUnits);
+            inputStreamController.add('recompile ${dart2js.path} x$count\n'
+                '${dart2jsOtherFile.path}\n'
+                'x$count\n'
+                .codeUnits);
           } else if (count == 3) {
             // Partial file. Expect to not be empty.
             expect(incrementalDillFile.existsSync(), equals(true));