dart2js support for loading .dill files built modularly

Change-Id: Ie360b13b5be786df9101c96982400136c63dff00
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/97462
Commit-Queue: Sigmund Cherem <sigmund@google.com>
Reviewed-by: Jens Johansen <jensj@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/compiler/lib/src/commandline_options.dart b/pkg/compiler/lib/src/commandline_options.dart
index 704cfe7..82b1fe8 100644
--- a/pkg/compiler/lib/src/commandline_options.dart
+++ b/pkg/compiler/lib/src/commandline_options.dart
@@ -79,6 +79,7 @@
   static const String progress = '--show-internal-progress';
   static const String version = '--version';
 
+  static const String dillDependencies = '--dill-dependencies';
   static const String readData = '--read-data';
   static const String writeData = '--write-data';
   static const String cfeOnly = '--cfe-only';
diff --git a/pkg/compiler/lib/src/dart2js.dart b/pkg/compiler/lib/src/dart2js.dart
index 0fe8224..3d64064 100644
--- a/pkg/compiler/lib/src/dart2js.dart
+++ b/pkg/compiler/lib/src/dart2js.dart
@@ -250,12 +250,18 @@
       fail("Cannot read and write serialized simultaneously.");
     }
     if (argument != Flags.readData) {
-      readDataUri = currentDirectory
-          .resolve(nativeToUriPath(extractPath(argument, isDirectory: false)));
+      readDataUri = nativeToUri(extractPath(argument, isDirectory: false));
     }
     compilationStrategy = CompilationStrategy.fromData;
   }
 
+  void setDillDependencies(String argument) {
+    String dependencies = extractParameter(argument);
+    String uriDependencies = dependencies.splitMapJoin(',',
+        onMatch: (_) => ',', onNonMatch: (p) => '${nativeToUri(p)}');
+    options.add('${Flags.dillDependencies}=${uriDependencies}');
+  }
+
   void setCfeOnly(String argument) {
     compilationStrategy = CompilationStrategy.toKernel;
   }
@@ -265,8 +271,7 @@
       fail("Cannot read and write serialized simultaneously.");
     }
     if (argument != Flags.writeData) {
-      writeDataUri = currentDirectory
-          .resolve(nativeToUriPath(extractPath(argument, isDirectory: false)));
+      writeDataUri = nativeToUri(extractPath(argument, isDirectory: false));
     }
     compilationStrategy = CompilationStrategy.toData;
   }
@@ -341,6 +346,7 @@
     new OptionHandler(Flags.version, (_) => wantVersion = true),
     new OptionHandler('--library-root=.+', ignoreOption),
     new OptionHandler('--libraries-spec=.+', setLibrarySpecificationUri),
+    new OptionHandler('${Flags.dillDependencies}=.+', setDillDependencies),
     new OptionHandler('${Flags.readData}|${Flags.readData}=.+', setReadData),
     new OptionHandler('${Flags.writeData}|${Flags.writeData}=.+', setWriteData),
     new OptionHandler(Flags.cfeOnly, setCfeOnly),
diff --git a/pkg/compiler/lib/src/filenames.dart b/pkg/compiler/lib/src/filenames.dart
index 32aca53..be671f9 100644
--- a/pkg/compiler/lib/src/filenames.dart
+++ b/pkg/compiler/lib/src/filenames.dart
@@ -31,4 +31,7 @@
 
 final Uri currentDirectory = Uri.base;
 
+Uri nativeToUri(String filename) =>
+    currentDirectory.resolve(nativeToUriPath(filename));
+
 String appendSlash(String path) => path.endsWith('/') ? path : '$path/';
diff --git a/pkg/compiler/lib/src/kernel/loader.dart b/pkg/compiler/lib/src/kernel/loader.dart
index b972976..f73fba4 100644
--- a/pkg/compiler/lib/src/kernel/loader.dart
+++ b/pkg/compiler/lib/src/kernel/loader.dart
@@ -57,22 +57,48 @@
   /// Loads an entire Kernel [Component] from a file on disk.
   Future<KernelResult> load(Uri resolvedUri) {
     return measure(() async {
+      String targetName =
+          _options.compileForServer ? "dart2js_server" : "dart2js";
+      String platform = '${targetName}_platform.dill';
       var isDill = resolvedUri.path.endsWith('.dill');
       ir.Component component;
       if (isDill) {
-        api.Input input = await _compilerInput.readFromUri(resolvedUri,
-            inputKind: api.InputKind.binary);
         component = new ir.Component();
-        new BinaryBuilder(input.data).readComponent(component);
+        Future<void> read(Uri uri) async {
+          api.Input input = await _compilerInput.readFromUri(uri,
+              inputKind: api.InputKind.binary);
+          new BinaryBuilder(input.data).readComponent(component);
+        }
+
+        await read(resolvedUri);
+        if (_options.dillDependencies != null) {
+          // Modular compiles do not include the platform on the input dill
+          // either.
+          await read(_options.platformBinaries.resolve(platform));
+          for (Uri dependency in _options.dillDependencies) {
+            await read(dependency);
+          }
+        }
+
+        // This is not expected to be null when creating a whole-program .dill
+        // file, but needs to be checked for modular inputs.
+        if (component.mainMethod == null) {
+          // TODO(sigmund): move this so that we use the same error template
+          // from the CFE.
+          _reporter.reportError(_reporter.createMessage(NO_LOCATION_SPANNABLE,
+              MessageKind.GENERIC, {'text': "No 'main' method found."}));
+          return null;
+        }
       } else {
-        String targetName =
-            _options.compileForServer ? "dart2js_server" : "dart2js";
-        String platform = '${targetName}_platform.dill';
+        List<Uri> dependencies = [_options.platformBinaries.resolve(platform)];
+        if (_options.dillDependencies != null) {
+          dependencies.addAll(_options.dillDependencies);
+        }
         initializedCompilerState = fe.initializeCompiler(
             initializedCompilerState,
             new Dart2jsTarget(targetName, new TargetFlags()),
             _options.librariesSpecificationUri,
-            _options.platformBinaries.resolve(platform),
+            dependencies,
             _options.packageConfig,
             experimentalFlags: _options.languageExperiments);
         component = await fe.compile(
diff --git a/pkg/compiler/lib/src/options.dart b/pkg/compiler/lib/src/options.dart
index a3acf0e..8a6e462 100644
--- a/pkg/compiler/lib/src/options.dart
+++ b/pkg/compiler/lib/src/options.dart
@@ -51,6 +51,19 @@
   /// If not null then [packageRoot] should be null.
   Uri packageConfig;
 
+  /// List of kernel files to load.
+  ///
+  /// When compiling modularly, this contains kernel files that are needed
+  /// to compile a single module.
+  ///
+  /// When linking, this contains all kernel files that form part of the final
+  /// program.
+  ///
+  /// At this time, this list points to full kernel files. In the future, we may
+  /// use a list of outline files for modular compiles, and only use full kernel
+  /// files for linking.
+  List<Uri> dillDependencies;
+
   /// Location from which serialized inference data is read.
   ///
   /// If this is set, the [entryPoint] is expected to be a .dill file and the
@@ -374,6 +387,8 @@
       ..useNewSourceInfo = _hasOption(options, Flags.useNewSourceInfo)
       ..verbose = _hasOption(options, Flags.verbose)
       ..showInternalProgress = _hasOption(options, Flags.progress)
+      ..dillDependencies =
+          _extractUriListOption(options, '${Flags.dillDependencies}')
       ..readDataUri = _extractUriOption(options, '${Flags.readData}=')
       ..writeDataUri = _extractUriOption(options, '${Flags.writeData}=')
       ..cfeOnly = _hasOption(options, Flags.cfeOnly)
@@ -526,6 +541,15 @@
   return null;
 }
 
+/// Extract list of comma separated Uris provided for [flag]. Returns an
+/// empty list if [option] contain [flag] without arguments. Returns `null` if
+/// [option] doesn't contain [flag] with or without arguments.
+List<Uri> _extractUriListOption(List<String> options, String flag) {
+  List<String> stringUris = _extractOptionalCsvOption(options, flag);
+  if (stringUris == null) return null;
+  return stringUris.map(Uri.parse).toList();
+}
+
 Map<fe.ExperimentalFlag, bool> _extractExperiments(List<String> options) {
   List<String> experiments =
       _extractOptionalCsvOption(options, Flags.enableLanguageExperiments);
diff --git a/pkg/front_end/lib/src/api_unstable/dart2js.dart b/pkg/front_end/lib/src/api_unstable/dart2js.dart
index a08b77d..37129d0 100644
--- a/pkg/front_end/lib/src/api_unstable/dart2js.dart
+++ b/pkg/front_end/lib/src/api_unstable/dart2js.dart
@@ -109,9 +109,10 @@
     InitializedCompilerState oldState,
     Target target,
     Uri librariesSpecificationUri,
-    Uri sdkPlatformUri,
+    List<Uri> linkedDependencies,
     Uri packagesFileUri,
-    {Map<ExperimentalFlag, bool> experimentalFlags}) {
+    {List<Uri> dependencies,
+    Map<ExperimentalFlag, bool> experimentalFlags}) {
   bool mapEqual(Map<ExperimentalFlag, bool> a, Map<ExperimentalFlag, bool> b) {
     if (a == null || b == null) return a == b;
     if (a.length != b.length) return false;
@@ -121,10 +122,20 @@
     return true;
   }
 
+  bool listEqual(List<Uri> a, List<Uri> b) {
+    if (a.length != b.length) return false;
+    for (int i = 0; i < a.length; ++i) {
+      if (a[i] != b[i]) return false;
+    }
+    return true;
+  }
+
+  linkedDependencies.sort((a, b) => a.toString().compareTo(b.toString()));
+
   if (oldState != null &&
       oldState.options.packagesFileUri == packagesFileUri &&
       oldState.options.librariesSpecificationUri == librariesSpecificationUri &&
-      oldState.options.linkedDependencies[0] == sdkPlatformUri &&
+      listEqual(oldState.options.linkedDependencies, linkedDependencies) &&
       mapEqual(oldState.options.experimentalFlags, experimentalFlags)) {
     return oldState;
   }
@@ -132,7 +143,7 @@
   CompilerOptions options = new CompilerOptions()
     ..target = target
     ..legacyMode = target.legacyMode
-    ..linkedDependencies = [sdkPlatformUri]
+    ..linkedDependencies = linkedDependencies
     ..librariesSpecificationUri = librariesSpecificationUri
     ..packagesFileUri = packagesFileUri
     ..experimentalFlags = experimentalFlags;
diff --git a/tests/compiler/dart2js/end_to_end/modular_loader_test.dart b/tests/compiler/dart2js/end_to_end/modular_loader_test.dart
new file mode 100644
index 0000000..b1a1d98
--- /dev/null
+++ b/tests/compiler/dart2js/end_to_end/modular_loader_test.dart
@@ -0,0 +1,136 @@
+// Copyright (c) 2019, 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 '../helpers/memory_compiler.dart';
+import 'package:async_helper/async_helper.dart';
+import 'package:compiler/src/apiimpl.dart' show CompilerImpl;
+import 'package:compiler/src/common_elements.dart';
+import 'package:compiler/src/elements/entities.dart'
+    show LibraryEntity, ClassEntity;
+import 'package:compiler/src/kernel/dart2js_target.dart';
+import 'package:compiler/src/kernel/loader.dart';
+import 'package:expect/expect.dart';
+import 'package:front_end/src/api_prototype/front_end.dart';
+import 'package:front_end/src/api_prototype/memory_file_system.dart';
+import 'package:front_end/src/api_prototype/standard_file_system.dart';
+import 'package:front_end/src/compute_platform_binaries_location.dart'
+    show computePlatformBinariesLocation;
+import 'package:front_end/src/fasta/kernel/utils.dart' show serializeComponent;
+import 'package:kernel/ast.dart';
+import 'package:kernel/target/targets.dart' show TargetFlags;
+
+/// Test that the compiler can load kernel in modular fragments.
+main() {
+  asyncTest(() async {
+    var aDill = await compileUnit(['a0.dart'], {'a0.dart': sourceA});
+    var bDill = await compileUnit(
+        ['b1.dart'], {'b1.dart': sourceB, 'a.dill': aDill},
+        deps: ['a.dill']);
+    var cDill = await compileUnit(
+        ['c2.dart'], {'c2.dart': sourceC, 'a.dill': aDill, 'b.dill': bDill},
+        deps: ['a.dill', 'b.dill']);
+
+    DiagnosticCollector diagnostics = new DiagnosticCollector();
+    OutputCollector output = new OutputCollector();
+    Uri entryPoint = Uri.parse('memory:c.dill');
+    CompilerImpl compiler = compilerFor(
+        entryPoint: entryPoint,
+        options: ['--dill-dependencies=memory:a.dill,memory:b.dill'],
+        memorySourceFiles: {'a.dill': aDill, 'b.dill': bDill, 'c.dill': cDill},
+        diagnosticHandler: diagnostics,
+        outputProvider: output);
+    await compiler.setupSdk();
+    KernelResult result = await compiler.kernelLoader.load(entryPoint);
+    compiler.frontendStrategy.registerLoadedLibraries(result);
+
+    Expect.equals(0, diagnostics.errors.length);
+    Expect.equals(0, diagnostics.warnings.length);
+
+    ElementEnvironment environment =
+        compiler.frontendStrategy.elementEnvironment;
+    LibraryEntity library = environment.lookupLibrary(toTestUri('b1.dart'));
+    Expect.isNotNull(library);
+    ClassEntity clss = environment.lookupClass(library, 'B1');
+    Expect.isNotNull(clss);
+    var member = environment.lookupClassMember(clss, 'foo');
+    Expect.isNotNull(member);
+  });
+}
+
+/// Generate a component for a modular complation unit.
+Future<List<int>> compileUnit(List<String> inputs, Map<String, dynamic> sources,
+    {List<String> deps: const []}) async {
+  var fs = new MemoryFileSystem(_defaultDir);
+  sources.forEach((name, data) {
+    var entity = fs.entityForUri(toTestUri(name));
+    if (data is String) {
+      entity.writeAsStringSync(data);
+    } else {
+      entity.writeAsBytesSync(data);
+    }
+  });
+  List<Uri> linkedDependencies = [
+    computePlatformBinariesLocation().resolve("dart2js_platform.dill"),
+  ]..addAll(deps.map(toTestUri));
+  fs.entityForUri(toTestUri('.packages')).writeAsStringSync('');
+  var options = new CompilerOptions()
+    ..target = new Dart2jsTarget("dart2js", new TargetFlags())
+    ..fileSystem = new TestFileSystem(fs)
+    ..linkedDependencies = linkedDependencies
+    ..packagesFileUri = toTestUri('.packages');
+  var inputUris = inputs.map(toTestUri).toList();
+  var inputUriSet = inputUris.toSet();
+  var component = await kernelForComponent(inputUris, options);
+  for (var lib in component.libraries) {
+    if (!inputUriSet.contains(lib.importUri)) {
+      component.root.getChildFromUri(lib.importUri).bindTo(lib.reference);
+      lib.computeCanonicalNames();
+    }
+  }
+  return serializeComponent(component,
+      filter: (Library lib) => inputUriSet.contains(lib.importUri));
+}
+
+Uri _defaultDir = Uri.parse('org-dartlang-test:///');
+
+Uri toTestUri(String relativePath) => _defaultDir.resolve(relativePath);
+
+class TestFileSystem implements FileSystem {
+  final MemoryFileSystem memory;
+  final FileSystem physical = StandardFileSystem.instance;
+
+  TestFileSystem(this.memory);
+
+  @override
+  FileSystemEntity entityForUri(Uri uri) {
+    if (uri.scheme == 'file') return physical.entityForUri(uri);
+    return memory.entityForUri(uri);
+  }
+}
+
+const sourceA = '''
+class A0 {
+  StringBuffer buffer = new StringBuffer();
+}
+''';
+
+const sourceB = '''
+import 'a0.dart';
+
+class B1 extends A0 {
+  A0 get foo => null;
+}
+
+A0 createA0() => new A0();
+''';
+
+const sourceC = '''
+import 'b1.dart';
+
+class C2 extends B1 {
+  final foo = createA0();
+}
+
+main() => print(new C2().foo.buffer.toString());
+''';