Version 2.14.0-234.0.dev

Merge commit '5b5956bd4e0332ed65e3579f3b7a78d0a7bddcb7' into 'dev'
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
diff --git a/tools/VERSION b/tools/VERSION
index 6288f25..da6d6f2 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 14
 PATCH 0
-PRERELEASE 233
+PRERELEASE 234
 PRERELEASE_PATCH 0
\ No newline at end of file