[CFE] First pass at internal test utility

This is a first pass at running inlined tests
(seemingly called doctests in for instance python) on internal CFE code.

This is not useful for testing the entire compiler for instance, but
could be useful for utility methods that take simple inputs and provide
simple outputs.

I've added tests to a few tings to try it out.

The syntax for it here is probably overly verbose but we could iterate
on that if need be.

The way it's done is that the test-code is extracted from the comments
and (virtually) "injected" in a weirdly named top level method of the
same file, which is then called from a "fake" main; the whole thing is
compiled to dill and run in an isolate.
(This is an application where making advanced invalidation support
adding top level methods would be great).

Change-Id: I7927822bca0ba68780458c40fe585882330e3b60
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/203242
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Dmitry Stefantsov <dmitryas@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/front_end/lib/src/fasta/incremental_compiler.dart b/pkg/front_end/lib/src/fasta/incremental_compiler.dart
index e4dbd47..0cc4f5d 100644
--- a/pkg/front_end/lib/src/fasta/incremental_compiler.dart
+++ b/pkg/front_end/lib/src/fasta/incremental_compiler.dart
@@ -2249,6 +2249,55 @@
 }
 
 /// Translate a parts "partUri" to an actual uri with handling of invalid uris.
+///
+/// ```
+/// DartDocTest(
+///   getPartUri(
+///     Uri.parse("file://path/to/parent.dart"),
+///     new LibraryPart([], "simple.dart")
+///   ),
+///   Uri.parse("file://path/to/simple.dart")
+/// )
+/// DartDocTest(
+///   getPartUri(
+///     Uri.parse("file://path/to/parent.dart"),
+///     new LibraryPart([], "dir/simple.dart")
+///   ),
+///   Uri.parse("file://path/to/dir/simple.dart")
+/// )
+/// DartDocTest(
+///   getPartUri(
+///     Uri.parse("file://path/to/parent.dart"),
+///     new LibraryPart([], "../simple.dart")
+///   ),
+///   Uri.parse("file://path/simple.dart")
+/// )
+/// DartDocTest(
+///   getPartUri(
+///     Uri.parse("file://path/to/parent.dart"),
+///     new LibraryPart([], "file:///my/path/absolute.dart")
+///   ),
+///   Uri.parse("file:///my/path/absolute.dart")
+/// )
+/// DartDocTest(
+///   getPartUri(
+///     Uri.parse("file://path/to/parent.dart"),
+///     new LibraryPart([], "package:foo/hello.dart")
+///   ),
+///   Uri.parse("package:foo/hello.dart")
+/// )
+/// ```
+/// And with invalid part uri:
+/// ```
+/// DartDocTest(
+///   getPartUri(
+///     Uri.parse("file://path/to/parent.dart"),
+///     new LibraryPart([], ":hello")
+///   ),
+///   new Uri(scheme: SourceLibraryBuilder.MALFORMED_URI_SCHEME,
+///     query: Uri.encodeQueryComponent(":hello"))
+/// )
+/// ```
 Uri getPartUri(Uri parentUri, LibraryPart part) {
   try {
     return parentUri.resolve(part.partUri);
diff --git a/pkg/front_end/lib/src/fasta/kernel/body_builder.dart b/pkg/front_end/lib/src/fasta/kernel/body_builder.dart
index 9e7c46c6..953a360 100644
--- a/pkg/front_end/lib/src/fasta/kernel/body_builder.dart
+++ b/pkg/front_end/lib/src/fasta/kernel/body_builder.dart
@@ -6867,9 +6867,25 @@
   }
 }
 
+/// DartDocTest(
+///   debugName("myClassName", "myName", "myPrefix"),
+///   "myPrefix.myClassName.myName"
+/// )
+/// DartDocTest(
+///   debugName("myClassName", "myName"),
+///   "myClassName.myName"
+/// )
+/// DartDocTest(
+///   debugName("myClassName", ""),
+///   "myClassName"
+/// )
+/// DartDocTest(
+///   debugName("", ""),
+///   ""
+/// )
 String debugName(String className, String name, [String prefix]) {
   String result = name.isEmpty ? className : "$className.$name";
-  return prefix == null ? result : "$prefix.result";
+  return prefix == null ? result : "$prefix.$result";
 }
 
 // TODO(johnniwinther): This is a bit ad hoc. Call sites should know what kind
diff --git a/pkg/front_end/test/binary_md_dill_reader.dart b/pkg/front_end/test/binary_md_dill_reader.dart
index 7894e3c..f427697 100644
--- a/pkg/front_end/test/binary_md_dill_reader.dart
+++ b/pkg/front_end/test/binary_md_dill_reader.dart
@@ -6,74 +6,6 @@
 
 import "dart:math" as math;
 
-main() {
-  List<String> test = BinaryMdDillReader._getGenerics("Pair<A, B>");
-  if (test.length != 2 || test[0] != "A" || test[1] != "B") {
-    throw "Expected [A, B] got $test";
-  }
-
-  test = BinaryMdDillReader._getGenerics("List<Expression>");
-  if (test.length != 1 || test[0] != "Expression") {
-    throw "Expected [Expression] got $test";
-  }
-
-  test = BinaryMdDillReader._getGenerics("List<Pair<FileOffset, Expression>>");
-  if (test.length != 1 || test[0] != "Pair<FileOffset, Expression>") {
-    throw "Expected [Pair<FileOffset, Expression>] got $test";
-  }
-
-  test = BinaryMdDillReader._getGenerics("RList<Pair<UInt32, UInt32>>");
-  if (test.length != 1 || test[0] != "Pair<UInt32, UInt32>") {
-    throw "Expected [Pair<UInt32, UInt32>] got $test";
-  }
-
-  test =
-      BinaryMdDillReader._getGenerics("List<Pair<FieldReference, Expression>>");
-  if (test.length != 1 || test[0] != "Pair<FieldReference, Expression>") {
-    throw "Expected [Pair<FieldReference, Expression>] got $test";
-  }
-
-  test = BinaryMdDillReader._getGenerics(
-      "List<Pair<ConstantReference, ConstantReference>>");
-  if (test.length != 1 ||
-      test[0] != "Pair<ConstantReference, ConstantReference>") {
-    throw "Expected [Pair<ConstantReference, ConstantReference>] got $test";
-  }
-
-  test = BinaryMdDillReader._getGenerics(
-      "List<Pair<FieldReference, ConstantReference>>");
-  if (test.length != 1 ||
-      test[0] != "Pair<FieldReference, ConstantReference>") {
-    throw "Expected [Pair<FieldReference, ConstantReference>] got $test";
-  }
-
-  test = BinaryMdDillReader._getGenerics("Option<List<DartType>>");
-  if (test.length != 1 || test[0] != "List<DartType>") {
-    throw "Expected [List<DartType>] got $test";
-  }
-
-  test = BinaryMdDillReader._getGenerics("Foo<Bar<Baz>>");
-  if (test.length != 1 || test[0] != "Bar<Baz>") {
-    throw "Expected [Bar<Baz>] got $test";
-  }
-
-  test = BinaryMdDillReader._getGenerics("Foo<A, B<C, D>, E>");
-  if (test.length != 3 ||
-      test[0] != "A" ||
-      test[1] != "B<C, D>" ||
-      test[2] != "E") {
-    throw "Expected [A, B<C, D>, E] got $test";
-  }
-
-  test = BinaryMdDillReader._getGenerics("Foo<A, B<C, D<E<F<G>>>>, H>");
-  if (test.length != 3 ||
-      test[0] != "A" ||
-      test[1] != "B<C, D<E<F<G>>>>" ||
-      test[2] != "H") {
-    throw "Expected [A, B<C, D<E<F<G>>>>, H] got $test";
-  }
-}
-
 class BinaryMdDillReader {
   final String _binaryMdContent;
 
@@ -317,6 +249,65 @@
   ///
   /// Note that the input string *has* to use generics, i.e. have '<' and '>'
   /// in it.
+  ///
+  /// DartDocTest(
+  ///   BinaryMdDillReader._getGenerics("Pair<A, B>"), ["A", "B"]
+  /// )
+  /// DartDocTest(
+  ///   BinaryMdDillReader._getGenerics("List<Expression>"), ["Expression"]
+  /// )
+  /// DartDocTest(
+  ///   BinaryMdDillReader._getGenerics("List<Pair<FileOffset, Expression>>"),
+  ///   ["Pair<FileOffset, Expression>"]
+  /// )
+  /// DartDocTest(
+  ///   BinaryMdDillReader._getGenerics("RList<Pair<UInt32, UInt32>>"),
+  ///   ["Pair<UInt32, UInt32>"]
+  /// )
+  /// DartDocTest(
+  ///   BinaryMdDillReader._getGenerics(
+  ///     "List<Pair<FieldReference, Expression>>"),
+  ///   ["Pair<FieldReference, Expression>"]
+  /// )
+  /// DartDocTest(
+  ///   BinaryMdDillReader._getGenerics(
+  ///     "List<Pair<ConstantReference, ConstantReference>>"),
+  ///   ["Pair<ConstantReference, ConstantReference>"]
+  /// )
+  /// DartDocTest(
+  ///   BinaryMdDillReader._getGenerics(
+  ///     "List<Pair<FieldReference, ConstantReference>>"),
+  ///   ["Pair<FieldReference, ConstantReference>"]
+  /// )
+  /// DartDocTest(
+  ///   BinaryMdDillReader._getGenerics("Option<List<DartType>>"),
+  ///   ["List<DartType>"]
+  /// )
+  /// DartDocTest(
+  ///   BinaryMdDillReader._getGenerics("Foo<Bar<Baz>>"), ["Bar<Baz>"]
+  /// )
+  /// DartDocTest(
+  ///      BinaryMdDillReader._getGenerics("Foo<A, B<C, D>, E>"),
+  ///            ["A", "B<C, D>", "E"]
+  /// )
+  /// DartDocTest(
+  ///      BinaryMdDillReader._getGenerics("Foo<A, B<C, D<E<F<G>>>>, H>"),
+  ///            ["A", "B<C, D<E<F<G>>>>", "H"]
+  /// )
+  ///
+  /// Expected failing run (expects to fail with unbalanced < >).
+  /// TODO: Support this more elegantly.
+  /// DartDocTest(() {
+  ///      try {
+  ///        BinaryMdDillReader._getGenerics("Foo<A, B<C, D, E>");
+  ///        return false;
+  ///      } catch(e) {
+  ///        return true;
+  ///      }
+  ///    }(),
+  ///    true
+  /// )
+  ///
   static List<String> _getGenerics(String s) {
     s = s.substring(s.indexOf("<") + 1, s.lastIndexOf(">"));
     // Check that any '<' and '>' are balanced and split entries on comma for
diff --git a/pkg/front_end/test/dartdoctest_suite.dart b/pkg/front_end/test/dartdoctest_suite.dart
new file mode 100644
index 0000000..77266f1
--- /dev/null
+++ b/pkg/front_end/test/dartdoctest_suite.dart
@@ -0,0 +1,71 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// @dart = 2.9
+
+import 'package:testing/testing.dart'
+    show Chain, ChainContext, Result, Step, TestDescription, runMe;
+
+import '../tool/dart_doctest_impl.dart';
+
+main([List<String> arguments = const []]) =>
+    runMe(arguments, createContext, configurationPath: "../testing.json");
+
+Future<Context> createContext(
+    Chain suite, Map<String, String> environment) async {
+  return new Context(suite.name);
+}
+
+class Context extends ChainContext {
+  final String suiteName;
+
+  Context(this.suiteName);
+
+  final List<Step> steps = const <Step>[
+    const DartDocTestStep(),
+  ];
+
+  // Override special handling of negative tests.
+  @override
+  Result processTestResult(
+      TestDescription description, Result result, bool last) {
+    return result;
+  }
+
+  Stream<DartDocTestTestDescription> list(Chain suite) async* {
+    await for (TestDescription entry in super.list(suite)) {
+      List<Test> tests = extractTestsFromUri(entry.uri);
+      if (tests.isEmpty) continue;
+      yield new DartDocTestTestDescription(entry.shortName, entry.uri, tests);
+    }
+  }
+
+  DartDocTest dartDocTest = new DartDocTest();
+}
+
+class DartDocTestTestDescription extends TestDescription {
+  final String shortName;
+  final Uri uri;
+  final List<Test> tests;
+
+  DartDocTestTestDescription(this.shortName, this.uri, this.tests);
+}
+
+class DartDocTestStep extends Step<DartDocTestTestDescription,
+    DartDocTestTestDescription, Context> {
+  const DartDocTestStep();
+
+  String get name => "DartDocTest";
+
+  Future<Result<DartDocTestTestDescription>> run(
+      DartDocTestTestDescription description, Context context) async {
+    bool result = await context.dartDocTest
+        .compileAndRun(description.uri, description.tests);
+    if (result) {
+      return new Result<DartDocTestTestDescription>.pass(description);
+    } else {
+      return new Result<DartDocTestTestDescription>.fail(description);
+    }
+  }
+}
diff --git a/pkg/front_end/test/dartdoctest_suite.status b/pkg/front_end/test/dartdoctest_suite.status
new file mode 100644
index 0000000..48716a2
--- /dev/null
+++ b/pkg/front_end/test/dartdoctest_suite.status
@@ -0,0 +1,4 @@
+# Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE.md file.
+
diff --git a/pkg/front_end/test/spell_checking_list_tests.txt b/pkg/front_end/test/spell_checking_list_tests.txt
index 58478ccb..e88b901 100644
--- a/pkg/front_end/test/spell_checking_list_tests.txt
+++ b/pkg/front_end/test/spell_checking_list_tests.txt
@@ -28,6 +28,8 @@
 allowlist
 allowlisting
 alt
+amend
+amended
 amortized
 analyses
 animal
@@ -147,6 +149,7 @@
 commented
 commit
 commits
+communication
 companion
 comparative
 comparer
@@ -189,7 +192,11 @@
 dacoharkes
 dadd
 daemon
+dart$doc$test$tester
+dart\$doc\$test\$tester
 dartanalyzer
+dartdoctest
+dartdoctestrun
 dartfile
 dartfmt
 dash
@@ -237,6 +244,7 @@
 dist
 div
 divergent
+doctest
 doctype
 doesnt
 dog
@@ -258,6 +266,7 @@
 echo
 edits
 elapse
+elegantly
 ell
 entrypoint
 entrypoints
diff --git a/pkg/front_end/test/unit_test_suites_impl.dart b/pkg/front_end/test/unit_test_suites_impl.dart
index c9d550c..5c6ccff 100644
--- a/pkg/front_end/test/unit_test_suites_impl.dart
+++ b/pkg/front_end/test/unit_test_suites_impl.dart
@@ -17,6 +17,7 @@
 import 'package:testing/src/suite.dart' as testing show Suite;
 import 'package:testing/src/test_description.dart' show TestDescription;
 
+import 'dartdoctest_suite.dart' as dartdoctest show createContext;
 import 'fasta/expression_suite.dart' as expression show createContext;
 import 'fasta/incremental_dartino_suite.dart' as incremental_dartino
     show createContext;
@@ -352,6 +353,12 @@
 
 const List<Suite> suites = [
   const Suite(
+    "dartdoctest",
+    dartdoctest.createContext,
+    "../testing.json",
+    shardCount: 1,
+  ),
+  const Suite(
     "fasta/expression",
     expression.createContext,
     "../../testing.json",
diff --git a/pkg/front_end/testing.json b/pkg/front_end/testing.json
index ab205e0..8c5b79e 100644
--- a/pkg/front_end/testing.json
+++ b/pkg/front_end/testing.json
@@ -270,6 +270,19 @@
       ]
     },
     {
+      "name": "dartdoctest",
+      "kind": "Chain",
+      "source": "test/dartdoctest_suite.dart",
+      "path": "../",
+      "status": "test/dartdoctest_suite.status",
+      "pattern": [
+        "_fe_analyzer_shared/.*\\.dart$",
+        "kernel/.*\\.dart$",
+        "front_end/.*\\.dart$"
+      ],
+      "exclude": []
+    },
+    {
       "name": "spelling_test_src",
       "kind": "Chain",
       "source": "test/spelling_test_src_suite.dart",
diff --git a/pkg/front_end/tool/dart_doctest_impl.dart b/pkg/front_end/tool/dart_doctest_impl.dart
new file mode 100644
index 0000000..7939fec
--- /dev/null
+++ b/pkg/front_end/tool/dart_doctest_impl.dart
@@ -0,0 +1,582 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'dart:typed_data';
+
+import 'package:_fe_analyzer_shared/src/scanner/scanner.dart'
+    show ScannerConfiguration;
+
+import 'package:_fe_analyzer_shared/src/scanner/token.dart';
+
+import 'package:_fe_analyzer_shared/src/scanner/utf8_bytes_scanner.dart'
+    show Utf8BytesScanner;
+
+import 'package:_fe_analyzer_shared/src/scanner/token.dart' show Token;
+import 'package:front_end/src/api_prototype/compiler_options.dart';
+import 'package:front_end/src/api_prototype/experimental_flags.dart';
+import 'package:front_end/src/api_prototype/file_system.dart';
+import 'package:front_end/src/api_prototype/standard_file_system.dart';
+import 'package:front_end/src/base/processed_options.dart';
+import 'package:front_end/src/fasta/compiler_context.dart';
+import 'package:front_end/src/fasta/fasta_codes.dart';
+// ignore: import_of_legacy_library_into_null_safe
+import 'package:front_end/src/fasta/incremental_compiler.dart';
+import 'package:front_end/src/fasta/kernel/utils.dart';
+import 'package:kernel/ast.dart';
+import 'package:kernel/target/targets.dart';
+// ignore: import_of_legacy_library_into_null_safe
+import 'package:vm/target/vm.dart';
+
+// ignore: import_of_legacy_library_into_null_safe
+import '../test/incremental_suite.dart' show getOptions;
+
+const _portMessageTest = "test";
+const _portMessageGood = "good";
+const _portMessageBad = "bad";
+const _portMessageBadDetails = "badDetails";
+const _portMessageCrash = "crash";
+const _portMessageParseError = "parseError";
+const _portMessageDone = "done";
+
+// TODO: This doesn't work on parts... (Well, it might, depending on how
+// the part declares what file it's part of and if we've compiled other stuff
+// first so we know more stuff).
+class DartDocTest {
+  IncrementalCompiler? incrementalCompiler;
+  late CompilerOptions options;
+  late ProcessedOptions processedOpts;
+  bool errors = false;
+
+  /// All-in-one. Process a file and return if it was good.
+  Future<bool> process(Uri uri) async {
+    print("\n\nProcessing $uri");
+    Stopwatch stopwatch = new Stopwatch()..start();
+
+    // Extract test cases in file.
+    List<Test> tests = extractTestsFromUri(uri);
+
+    if (tests.isEmpty) {
+      print("No tests found in file in ${stopwatch.elapsedMilliseconds} ms.");
+      return true;
+    }
+    print("Found ${tests.length} test(s) in file "
+        "in ${stopwatch.elapsedMilliseconds} ms.");
+
+    return await compileAndRun(uri, tests);
+  }
+
+  Future<bool> compileAndRun(Uri uri, List<Test> tests) async {
+    errors = false;
+
+    // Create code to amend the file with.
+    StringBuffer sb = new StringBuffer();
+    if (tests.isNotEmpty) {
+      sb.writeln(
+          r"Future<void> $dart$doc$test$tester(dynamic dartDocTest) async {");
+      for (Test test in tests) {
+        if (test is TestParseError) {
+          sb.writeln("dartDocTest.parseError(\"${test.message}\");");
+        } else {
+          sb.writeln("try {");
+          sb.writeln("  dartDocTest.test(${test.call}, ${test.result});");
+          sb.writeln("} catch (e) {");
+          sb.writeln("  dartDocTest.crash(e);");
+          sb.writeln("}");
+        }
+      }
+      sb.writeln("}");
+    }
+
+    if (incrementalCompiler == null) {
+      setupIncrementalCompiler(uri);
+    }
+
+    processedOpts.inputs.clear();
+    processedOpts.inputs.add(uri);
+    AmendedFileSystem fileSystem =
+        new AmendedFileSystem(StandardFileSystem.instance);
+    fileSystem.amendFileUri = uri;
+    fileSystem.amendWith = sb.toString();
+    options.fileSystem = fileSystem;
+    processedOpts.clearFileSystemCache();
+    // Invalidate file and package uri to force compilation and re-finding of
+    // packages (e.g. if we're now compiling somewhere else).
+    incrementalCompiler!.invalidate(uri);
+    incrementalCompiler!.invalidate(processedOpts.packagesUri);
+
+    Stopwatch stopwatch = new Stopwatch()..start();
+    Component component =
+        await incrementalCompiler!.computeDelta(entryPoints: [uri]);
+    if (errors) {
+      print("Got errors in ${stopwatch.elapsedMilliseconds} ms.");
+      return false;
+    }
+    print("Compiled (1) in ${stopwatch.elapsedMilliseconds} ms.");
+    stopwatch.reset();
+
+    fileSystem.amendImportUri = component.uriToSource[uri]?.importUri;
+    incrementalCompiler!.invalidate(AmendedFileSystem.mainUri);
+    Component componentMain = await incrementalCompiler!.computeDelta(
+        entryPoints: [AmendedFileSystem.mainUri], fullComponent: true);
+    if (errors) {
+      print("Got errors in ${stopwatch.elapsedMilliseconds} ms.");
+      return false;
+    }
+    print("Compiled (2) in ${stopwatch.elapsedMilliseconds} ms.");
+    stopwatch.reset();
+
+    Directory tmpDir = Directory.systemTemp.createTempSync();
+    Uri dillOutUri = tmpDir.uri.resolve("dartdoctestrun.dill");
+    await writeComponentToFile(componentMain, dillOutUri);
+    print("Wrote dill in ${stopwatch.elapsedMilliseconds} ms.");
+    stopwatch.reset();
+
+    // Spawn URI (dill uri) to run tests.
+    ReceivePort exitPort = new ReceivePort();
+    ReceivePort errorPort = new ReceivePort();
+    ReceivePort communicationPort = new ReceivePort();
+    Completer<dynamic> completer = new Completer();
+    bool error = false;
+    exitPort.listen((message) {
+      exitPort.close();
+      errorPort.close();
+      communicationPort.close();
+      completer.complete();
+    });
+    errorPort.listen((message) {
+      print("Isolate had an error: $message.");
+      error = true;
+      exitPort.close();
+      errorPort.close();
+      communicationPort.close();
+      completer.complete();
+    });
+    int testCount = 0;
+    int goodCount = 0;
+    int badCount = 0;
+    int crashCount = 0;
+    int parseErrorCount = 0;
+    bool done = false;
+    communicationPort.listen((message) {
+      if (done) {
+        throw "Didn't expect any more messages. Got '$message'";
+      }
+      if (message == _portMessageTest) {
+        testCount++;
+      } else if (message == _portMessageGood) {
+        goodCount++;
+      } else if (message == _portMessageBad) {
+        badCount++;
+      } else if (message.toString().startsWith("$_portMessageBadDetails: ")) {
+        print(message.toString().substring("$_portMessageBadDetails: ".length));
+      } else if (message == _portMessageCrash) {
+        crashCount++;
+      } else if (message.toString().startsWith("$_portMessageParseError: ")) {
+        parseErrorCount++;
+        print(message.toString().substring("$_portMessageParseError: ".length));
+      } else if (message == _portMessageDone) {
+        done = true;
+        // don't complete completer here. Expect the exit port to close.
+      } else {
+        throw "Didn't expect '$message'";
+      }
+    });
+    // TODO: Possibly it should be launched in a process instead so we can
+    // (sort of) catch and report exit calls in test code.
+    await Isolate.spawnUri(
+      dillOutUri,
+      [],
+      communicationPort.sendPort,
+      onExit: exitPort.sendPort,
+      onError: errorPort.sendPort,
+    );
+    await completer.future;
+    tmpDir.deleteSync(recursive: true);
+    if (error) {
+      print("Completed with an error in ${stopwatch.elapsedMilliseconds} ms.");
+      return false;
+    } else if (!done) {
+      print(
+          "Didn't complete correctly in ${stopwatch.elapsedMilliseconds} ms.");
+      return false;
+    } else if (testCount != tests.length) {
+      print("Didn't complete with error but ran "
+          "${testCount} tests while expecting ${tests.length} "
+          "in ${stopwatch.elapsedMilliseconds} ms.");
+      return false;
+    } else {
+      print("Processed $testCount test(s) "
+          "in ${stopwatch.elapsedMilliseconds} ms.");
+      if (goodCount == testCount &&
+          badCount == 0 &&
+          crashCount == 0 &&
+          parseErrorCount == 0) {
+        print("All tests passed.");
+        return true;
+      }
+      print("$goodCount OK; "
+          "$badCount bad; "
+          "$crashCount crashed; "
+          "$parseErrorCount parse errors.");
+      return false;
+    }
+  }
+
+  void setupIncrementalCompiler(Uri uri) {
+    options = getOptions();
+    TargetFlags targetFlags = new TargetFlags(enableNullSafety: true);
+    // TODO: Target could possible be something else...
+    Target target = new VmTarget(targetFlags);
+    options.target = target;
+    options.omitPlatform = true;
+    options.onDiagnostic = (DiagnosticMessage message) {
+      if (message.codeName == "InferredPackageUri") return;
+      print(message.plainTextFormatted.first);
+      if (message.severity == Severity.error) {
+        errors = true;
+      }
+    };
+
+    // Because we add a top-level method this doesn't currently do anything
+    // but maybe it will in the future.
+    options.explicitExperimentalFlags[
+        ExperimentalFlag.alternativeInvalidationStrategy] = true;
+
+    processedOpts = new ProcessedOptions(options: options, inputs: [uri]);
+    CompilerContext compilerContext = new CompilerContext(processedOpts);
+    this.incrementalCompiler = new IncrementalCompiler(compilerContext);
+  }
+}
+
+List<Test> extractTestsFromUri(Uri uri) {
+  // Extract test cases in file.
+  File file = new File.fromUri(uri);
+  Uint8List rawBytes = file.readAsBytesSync();
+  Uint8List bytes = new Uint8List(rawBytes.length + 1);
+  bytes.setRange(0, rawBytes.length, rawBytes);
+
+  ScannerConfiguration scannerConfiguration = new ScannerConfiguration(
+      enableExtensionMethods: true,
+      enableNonNullable: true,
+      enableTripleShift: true);
+
+  Utf8BytesScanner scanner = new Utf8BytesScanner(
+    bytes,
+    includeComments: true,
+    configuration: scannerConfiguration,
+    languageVersionChanged: (scanner, languageVersion) {
+      // For now don't do anything, but having it (making it non-null) means the
+      // configuration won't be reset.
+    },
+  );
+  Token firstToken = scanner.tokenize();
+  Token token = firstToken;
+  List<Test> tests = [];
+  while (!token.isEof) {
+    CommentToken? comment = token.precedingComments;
+    if (comment != null) {
+      tests.addAll(processComment(comment));
+      // TODO: Use parser to verify there's at least no syntax errors in the
+      // tests.
+    }
+    Token? next = token.next;
+    if (next == null) break;
+    token = next;
+  }
+  return tests;
+}
+
+const int $LF = 10;
+const int $OPEN_PAREN = 40;
+const int $CLOSE_PAREN = 41;
+const int $COMMA = 44;
+
+class Test {
+  final String call;
+  final String result;
+
+  Test(this.call, this.result);
+}
+
+class TestParseError implements Test {
+  final String message;
+
+  TestParseError(this.message);
+
+  @override
+  String get call => throw UnimplementedError();
+
+  @override
+  String get result => throw UnimplementedError();
+}
+
+List<Test> processComment(CommentToken comment) {
+  StringBuffer sb = new StringBuffer();
+  CommentToken? commentToken = comment;
+  bool commentBlock = false;
+  while (commentToken != null) {
+    String data = commentToken.lexeme.trim();
+    if (!commentBlock) {
+      if (data.startsWith("///")) {
+        data = data.substring(3).trim();
+      } else if (data.startsWith("//")) {
+        data = data.substring(2).trim();
+      } else if (data.startsWith("/*")) {
+        data = data.substring(2).trim();
+        commentBlock = true;
+      }
+    }
+
+    if (commentBlock && data.endsWith("*/")) {
+      data = data.substring(0, data.length - 2).trim();
+      commentBlock = false;
+    }
+    if (data.isNotEmpty) {
+      sb.write(data);
+    }
+    Token? next = commentToken.next;
+    commentToken = null;
+    if (next is CommentToken) commentToken = next;
+  }
+  String comments = sb.toString();
+  List<Test> result = [];
+  int index = comments.indexOf("DartDocTest(");
+  if (index < 0) {
+    return result;
+  }
+  List<int> codeUnits = comments.codeUnits;
+
+  while (index >= 0) {
+    // DartDocTest starts at (the current) $index --- now find the end,
+    // parenthesis, comma etc.
+    // TODO: This doesn't work for string literals with e.g. '(' and ',' in it.
+    int parenDepth = 0;
+    int firstParen = -1;
+    int commaAt = -1;
+    while (index < comments.length) {
+      int codeUnit = codeUnits[index++];
+      if (codeUnit == $OPEN_PAREN) {
+        parenDepth++;
+        if (parenDepth == 1) {
+          firstParen = index;
+        }
+      } else if (codeUnit == $CLOSE_PAREN) {
+        parenDepth--;
+        if (parenDepth == 0) {
+          break;
+        }
+      } else if (parenDepth == 1 && commaAt < 0 && codeUnit == $COMMA) {
+        commaAt = index;
+      }
+    }
+
+    int end = index;
+    if (parenDepth != 0 || firstParen < 0 || commaAt < 0) {
+      // TODO: Insert code-snippet that didn't parse...
+      result.add(new TestParseError("Parse error for test"));
+    } else {
+      result.add(new Test(
+        comments.substring(firstParen, commaAt - 1).trim(),
+        comments.substring(commaAt, end - 1).trim(),
+      ));
+    }
+    index = comments.indexOf("DartDocTest(", index);
+  }
+  return result;
+}
+
+class AmendedFileSystem implements FileSystem {
+  static Uri mainUri = Uri.parse("dartdoctest:main");
+  final FileSystem fs;
+  late Uri amendFileUri;
+  Uri? amendImportUri;
+  late String amendWith;
+
+  AmendedFileSystem(this.fs);
+
+  @override
+  FileSystemEntity entityForUri(Uri uri) {
+    if (uri == mainUri) {
+      return DartdoctestMainFile(amendImportUri ?? amendFileUri);
+    }
+    return new AmendedFileSystemEntity(
+        fs.entityForUri(uri), uri == amendFileUri ? amendWith : null);
+  }
+}
+
+class DartdoctestMainFile implements FileSystemEntity {
+  late final String content;
+  late final List<int> contentBytes = utf8.encode(content);
+
+  DartdoctestMainFile(Uri amendImportUri) {
+    content = """
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// @dart = 2.9
+
+import "$amendImportUri" as tester;
+import "dart:isolate";
+
+Future<void> main(List<String> args, SendPort port) async {
+  DartDocTest test = new DartDocTest(port);
+  await tester.\$dart\$doc\$test\$tester(test);
+  port.send("$_portMessageDone");
+}
+
+class DartDocTest {
+  final SendPort port;
+
+  DartDocTest(this.port);
+
+  void test(dynamic actual, dynamic expected) {
+    port.send("$_portMessageTest");
+    if (_testImpl(actual, expected)) {
+      port.send("$_portMessageGood");
+    } else {
+      port.send("$_portMessageBad");
+      port.send("$_portMessageBadDetails: Expected '\$expected'; got '\$actual'.");
+    }
+  }
+
+  void crash(dynamic error) {
+    port.send("$_portMessageTest");
+    port.send("$_portMessageCrash");
+  }
+
+  void parseError(String message) {
+    port.send("$_portMessageTest");
+    port.send("$_portMessageParseError: \$message");
+  }
+
+  bool _testImpl(dynamic actual, dynamic expected) {
+    if (identical(actual, expected)) return true;
+    if (actual == expected) return true;
+    if (actual == null || expected == null) return false;
+    if (actual is List && expected is List) {
+      if (actual.runtimeType != expected.runtimeType) return false;
+      if (actual.length != expected.length) return false;
+      for (int i = 0; i < actual.length; i++) {
+        if (actual[i] != expected[i]) return false;
+      }
+      return true;
+    }
+    if (actual is List || expected is List) return false;
+
+    if (actual is Map && expected is Map) {
+      if (actual.runtimeType != expected.runtimeType) return false;
+      if (actual.length != expected.length) return false;
+      for (dynamic key in actual.keys) {
+        if (!expected.containsKey(key)) return false;
+        if (actual[key] != expected[key]) return false;
+      }
+      return true;
+    }
+    if (actual is Map || expected is Map) return false;
+
+    if (actual is Set && expected is Set) {
+      if (actual.runtimeType != expected.runtimeType) return false;
+      if (actual.length != expected.length) return false;
+      for (dynamic value in actual) {
+        if (!expected.contains(value)) return false;
+      }
+      return true;
+    }
+    if (actual is Set || expected is Set) return false;
+
+    // More stuff?
+    return false;
+  }
+}
+""";
+  }
+
+  @override
+  Future<bool> exists() {
+    return Future.value(true);
+  }
+
+  @override
+  Future<bool> existsAsyncIfPossible() {
+    return Future.value(true);
+  }
+
+  @override
+  Future<List<int>> readAsBytes() {
+    return Future.value(contentBytes);
+  }
+
+  @override
+  Future<List<int>> readAsBytesAsyncIfPossible() {
+    return Future.value(contentBytes);
+  }
+
+  @override
+  Future<String> readAsString() {
+    return Future.value(content);
+  }
+
+  @override
+  Uri get uri => AmendedFileSystem.mainUri;
+}
+
+class AmendedFileSystemEntity implements FileSystemEntity {
+  final FileSystemEntity entityForUri;
+  final String? amendWith;
+  AmendedFileSystemEntity(this.entityForUri, this.amendWith);
+
+  @override
+  Future<bool> exists() {
+    return entityForUri.exists();
+  }
+
+  @override
+  Future<bool> existsAsyncIfPossible() {
+    return entityForUri.existsAsyncIfPossible();
+  }
+
+  @override
+  Future<List<int>> readAsBytes() async {
+    List<int> result = await entityForUri.readAsBytes();
+    return _amendIfNeeded(result);
+  }
+
+  List<int> _amendIfNeeded(List<int> existing) {
+    final String? amendWith = this.amendWith;
+    if (amendWith != null) {
+      List<int> encoded = utf8.encode(amendWith);
+      if (encoded.length > 0) {
+        Uint8List combined =
+            new Uint8List(existing.length + 1 + encoded.length);
+        combined.setRange(0, existing.length, existing);
+        combined[existing.length] = $LF;
+        combined.setRange(existing.length + 1, combined.length, encoded);
+        return combined;
+      }
+    }
+    return existing;
+  }
+
+  @override
+  Future<List<int>> readAsBytesAsyncIfPossible() async {
+    List<int> result = await entityForUri.readAsBytesAsyncIfPossible();
+    return _amendIfNeeded(result);
+  }
+
+  @override
+  Future<String> readAsString() async {
+    String result = await entityForUri.readAsString();
+    if (amendWith != null) return "$result\n$amendWith";
+    return result;
+  }
+
+  @override
+  Uri get uri => entityForUri.uri;
+}
diff --git a/pkg/front_end/tool/dart_doctest_runner.dart b/pkg/front_end/tool/dart_doctest_runner.dart
new file mode 100644
index 0000000..7791e66
--- /dev/null
+++ b/pkg/front_end/tool/dart_doctest_runner.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// @dart = 2.9
+
+import 'dart_doctest_impl.dart';
+
+Future<void> main(List<String> args) async {
+  DartDocTest dartDocTest = new DartDocTest();
+  for (String filename in args) {
+    await dartDocTest.process(Uri.base.resolve(filename));
+  }
+}
diff --git a/pkg/kernel/lib/import_table.dart b/pkg/kernel/lib/import_table.dart
index d6bf7ee..bf9f7d5 100644
--- a/pkg/kernel/lib/import_table.dart
+++ b/pkg/kernel/lib/import_table.dart
@@ -125,6 +125,34 @@
   }
 }
 
+/// DartDocTest(
+///   relativeUriPath(
+///     Uri.parse("file:///path/to/file1.dart"),
+///     Uri.parse("file:///path/to/file2.dart"),
+///   ),
+///   "file1.dart"
+/// )
+/// DartDocTest(
+///   relativeUriPath(
+///     Uri.parse("file:///path/to/a/file1.dart"),
+///     Uri.parse("file:///path/to/file2.dart"),
+///   ),
+///   "a/file1.dart"
+/// )
+/// DartDocTest(
+///   relativeUriPath(
+///     Uri.parse("file:///path/to/file1.dart"),
+///     Uri.parse("file:///path/to/b/file2.dart"),
+///   ),
+///   "../file1.dart"
+/// )
+/// DartDocTest(
+///   relativeUriPath(
+///     Uri.parse("file:///path/to/file1.dart"),
+///     Uri.parse("file:///different/path/to/file2.dart"),
+///   ),
+///   "../../../path/to/file1.dart"
+/// )
 String relativeUriPath(Uri target, Uri ref) {
   List<String> targetSegments = target.pathSegments;
   List<String> refSegments = ref.pathSegments;
diff --git a/pkg/kernel/lib/type_algebra.dart b/pkg/kernel/lib/type_algebra.dart
index c273494..4d28929 100644
--- a/pkg/kernel/lib/type_algebra.dart
+++ b/pkg/kernel/lib/type_algebra.dart
@@ -417,8 +417,86 @@
 ///
 /// Here `!` denotes `Nullability.nonNullable`, `?` denotes
 /// `Nullability.nullable`, `*` denotes `Nullability.legacy`, and `%` denotes
-/// `Nullability.neither`.  The table elements marked with N/A denote the
+/// `Nullability.undetermined`.  The table elements marked with N/A denote the
 /// cases that should yield a type error before the substitution is performed.
+///
+/// a is nonNullable:
+/// DartDocTest(
+///   combineNullabilitiesForSubstitution(
+///     Nullability.nonNullable, Nullability.nonNullable),
+///   Nullability.nonNullable
+/// )
+/// DartDocTest(
+///   combineNullabilitiesForSubstitution(
+///     Nullability.nonNullable, Nullability.nullable),
+///   Nullability.nullable
+/// )
+/// DartDocTest(
+///   combineNullabilitiesForSubstitution(
+///     Nullability.nonNullable, Nullability.legacy),
+///   Nullability.legacy
+/// )
+/// DartDocTest(
+///   combineNullabilitiesForSubstitution(
+///     Nullability.nonNullable, Nullability.undetermined),
+///   Nullability.nonNullable
+/// )
+///
+/// a is nullable:
+/// DartDocTest(
+///   combineNullabilitiesForSubstitution(
+///     Nullability.nullable, Nullability.nullable),
+///   Nullability.nullable
+/// )
+/// DartDocTest(
+///   combineNullabilitiesForSubstitution(
+///     Nullability.nullable, Nullability.legacy),
+///   Nullability.nullable
+/// )
+/// DartDocTest(
+///   combineNullabilitiesForSubstitution(
+///     Nullability.nullable, Nullability.undetermined),
+///   Nullability.nullable
+/// )
+///
+/// a is legacy:
+/// DartDocTest(
+///   combineNullabilitiesForSubstitution(
+///     Nullability.legacy, Nullability.nonNullable),
+///   Nullability.legacy
+/// )
+/// DartDocTest(
+///   combineNullabilitiesForSubstitution(
+///     Nullability.legacy, Nullability.nullable),
+///   Nullability.nullable
+/// )
+/// DartDocTest(
+///   combineNullabilitiesForSubstitution(
+///     Nullability.legacy, Nullability.legacy),
+///   Nullability.legacy
+/// )
+/// DartDocTest(
+///   combineNullabilitiesForSubstitution(
+///     Nullability.legacy, Nullability.undetermined),
+///   Nullability.legacy
+/// )
+///
+/// a is undetermined:
+/// DartDocTest(
+///   combineNullabilitiesForSubstitution(
+///     Nullability.undetermined, Nullability.nullable),
+///   Nullability.nullable
+/// )
+/// DartDocTest(
+///   combineNullabilitiesForSubstitution(
+///     Nullability.undetermined, Nullability.legacy),
+///   Nullability.legacy
+/// )
+/// DartDocTest(
+///   combineNullabilitiesForSubstitution(
+///     Nullability.undetermined, Nullability.undetermined),
+///   Nullability.undetermined
+/// )
 Nullability combineNullabilitiesForSubstitution(Nullability a, Nullability b) {
   // In the table above we may extend the function given by it, replacing N/A
   // with whatever is easier to implement.  In this implementation, we extend