// Copyright (c) 2016, 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. /// This is an interface to the Dart Kernel parser and Kernel binary generator. /// /// It is used by the kernel-isolate to load Dart source code and generate /// Kernel binary format. /// /// This is either invoked as the root script of the Kernel isolate when used /// as a part of /// /// dart --dfe=pkg/vm/bin/kernel_service.dart ... /// /// invocation or it is invoked as a standalone script to perform training for /// the app-jit snapshot /// /// dart pkg/vm/bin/kernel_service.dart --train /// /// library runtime.tools.kernel_service; import 'dart:async' show Future, ZoneSpecification, runZoned; import 'dart:io' show Platform, stderr hide FileSystemEntity; import 'dart:isolate'; import 'dart:typed_data' show Uint8List; import 'package:front_end/src/api_prototype/file_system.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'; import 'package:front_end/src/fasta/hybrid_file_system.dart'; import 'package:kernel/kernel.dart' show Component, Procedure; import 'package:kernel/target/targets.dart' show TargetFlags; import 'package:kernel/target/vm.dart' show VmTarget; import 'package:vm/incremental_compiler.dart'; const bool verbose = const bool.fromEnvironment('DFE_VERBOSE'); const String platformKernelFile = 'virtual_platform_kernel.dill'; // NOTE: Any changes to these tags need to be reflected in kernel_isolate.cc // Tags used to indicate different requests to the dart frontend. // // Current tags include the following: // 0 - Perform normal compilation. // 1 - Update in-memory file system with in-memory sources (used by tests). // 2 - Accept last compilation result. // 3 - APP JIT snapshot training run for kernel_service. // 4 - Compile an individual expression in some context (for debugging // purposes). const int kCompileTag = 0; const int kUpdateSourcesTag = 1; const int kAcceptTag = 2; const int kTrainTag = 3; const int kCompileExpressionTag = 4; bool allowDartInternalImport = false; abstract class Compiler { final FileSystem fileSystem; final bool strongMode; final List errors = new List(); CompilerOptions options; Compiler(this.fileSystem, Uri platformKernelPath, {this.strongMode: false, bool suppressWarnings: false, bool syncAsync: false, String packageConfig: null}) { Uri packagesUri = null; if (packageConfig != null) { packagesUri = Uri.parse(packageConfig); } else if (Platform.packageConfig != null) { packagesUri = Uri.parse(Platform.packageConfig); } if (verbose) { print("DFE: Platform.packageConfig: ${Platform.packageConfig}"); print("DFE: packagesUri: ${packagesUri}"); print("DFE: Platform.resolvedExecutable: ${Platform.resolvedExecutable}"); print("DFE: platformKernelPath: ${platformKernelPath}"); print("DFE: strongMode: ${strongMode}"); print("DFE: syncAsync: ${syncAsync}"); } options = new CompilerOptions() ..strongMode = strongMode ..fileSystem = fileSystem ..target = new VmTarget( new TargetFlags(strongMode: strongMode, syncAsync: syncAsync)) ..packagesFileUri = packagesUri ..sdkSummary = platformKernelPath ..verbose = verbose ..onProblem = (FormattedMessage message, Severity severity, List context) { bool printMessage; switch (severity) { case Severity.error: case Severity.internalProblem: // TODO(sigmund): support emitting code with errors as long as they // are handled in the generated code (issue #30194). printMessage = false; // errors are printed by VM errors.add(message.formatted); break; case Severity.nit: printMessage = false; break; case Severity.warning: printMessage = !suppressWarnings; break; case Severity.errorLegacyWarning: case Severity.context: throw "Unexpected severity: $severity"; } if (printMessage) { stderr.writeln(message.formatted); for (FormattedMessage message in context) { stderr.writeln(message.formatted); } } }; } Future compile(Uri script) { return runWithPrintToStderr(() => compileInternal(script)); } Future compileInternal(Uri script); } class IncrementalCompilerWrapper extends Compiler { IncrementalCompiler generator; IncrementalCompilerWrapper(FileSystem fileSystem, Uri platformKernelPath, {bool strongMode: false, bool suppressWarnings: false, bool syncAsync: false, String packageConfig: null}) : super(fileSystem, platformKernelPath, strongMode: strongMode, suppressWarnings: suppressWarnings, syncAsync: syncAsync, packageConfig: packageConfig); @override Future compileInternal(Uri script) async { if (generator == null) { generator = new IncrementalCompiler(options, script); } errors.clear(); return await generator.compile(entryPoint: script); } void accept() => generator.accept(); void invalidate(Uri uri) => generator.invalidate(uri); } class SingleShotCompilerWrapper extends Compiler { final bool requireMain; SingleShotCompilerWrapper(FileSystem fileSystem, Uri platformKernelPath, {this.requireMain: false, bool strongMode: false, bool suppressWarnings: false, bool syncAsync: false, String packageConfig: null}) : super(fileSystem, platformKernelPath, strongMode: strongMode, suppressWarnings: suppressWarnings, syncAsync: syncAsync, packageConfig: packageConfig); @override Future compileInternal(Uri script) async { return requireMain ? kernelForProgram(script, options) : kernelForComponent([script], options); } } final Map isolateCompilers = new Map(); IncrementalCompilerWrapper lookupIncrementalCompiler(int isolateId) { return isolateCompilers[isolateId]; } Future lookupOrBuildNewIncrementalCompiler(int isolateId, List sourceFiles, Uri platformKernelPath, List platformKernel, {bool strongMode: false, bool suppressWarnings: false, bool syncAsync: false, String packageConfig: null}) async { IncrementalCompilerWrapper compiler = lookupIncrementalCompiler(isolateId); if (compiler != null) { updateSources(compiler, sourceFiles); invalidateSources(compiler, sourceFiles); } else { final FileSystem fileSystem = sourceFiles.isEmpty && platformKernel == null ? StandardFileSystem.instance : _buildFileSystem(sourceFiles, platformKernel); // TODO(aam): IncrementalCompilerWrapper instance created below have to be // destroyed when corresponding isolate is shut down. To achieve that kernel // isolate needs to receive a message indicating that particular // isolate was shut down. Message should be handled here in this script. compiler = new IncrementalCompilerWrapper(fileSystem, platformKernelPath, strongMode: strongMode, suppressWarnings: suppressWarnings, syncAsync: syncAsync, packageConfig: packageConfig); isolateCompilers[isolateId] = compiler; } return compiler; } void updateSources(IncrementalCompilerWrapper compiler, List sourceFiles) { final bool hasMemoryFS = compiler.fileSystem is HybridFileSystem; if (sourceFiles.isNotEmpty) { final FileSystem fs = compiler.fileSystem; for (int i = 0; i < sourceFiles.length ~/ 2; i++) { Uri uri = Uri.parse(sourceFiles[i * 2]); List source = sourceFiles[i * 2 + 1]; // The source is only provided by unit tests and is normally empty. // Don't add an entry for the uri so the compiler will fallback to the // real file system for the updated source. if (hasMemoryFS && source != null) { (fs as HybridFileSystem) .memory .entityForUri(uri) .writeAsBytesSync(source); } } } } void invalidateSources(IncrementalCompilerWrapper compiler, List sourceFiles) { if (sourceFiles.isNotEmpty) { for (int i = 0; i < sourceFiles.length ~/ 2; i++) { compiler.invalidate(Uri.parse(sourceFiles[i * 2])); } } } // Process a request from the runtime. See KernelIsolate::CompileToKernel in // kernel_isolate.cc and Loader::SendKernelRequest in loader.cc. Future _processExpressionCompilationRequest(request) async { final SendPort port = request[1]; final int isolateId = request[2]; final String expression = request[3]; final List definitions = request[4]; final List typeDefinitions = request[5]; final String libraryUri = request[6]; final String klass = request[7]; // might be null final bool isStatic = request[8]; IncrementalCompilerWrapper compiler = isolateCompilers[isolateId]; if (compiler == null) { port.send(new CompilationResult.errors( ["No incremental compiler available for this isolate."], null) .toResponse()); return; } compiler.errors.clear(); CompilationResult result; try { Procedure procedure = await compiler.generator.compileExpression( expression, definitions, typeDefinitions, libraryUri, klass, isStatic); if (procedure == null) { port.send( new CompilationResult.errors(["Invalid scope."], null).toResponse()); return; } if (compiler.errors.isNotEmpty) { // TODO(sigmund): the compiler prints errors to the console, so we // shouldn't print those messages again here. result = new CompilationResult.errors(compiler.errors, null); } else { result = new CompilationResult.ok(serializeProcedure(procedure)); } } catch (error, stack) { result = new CompilationResult.crash(error, stack); } port.send(result.toResponse()); } Future _processLoadRequest(request) async { if (verbose) print("DFE: request: $request"); int tag = request[0]; if (tag == kCompileExpressionTag) { await _processExpressionCompilationRequest(request); return; } final SendPort port = request[1]; final String inputFileUri = request[2]; final Uri script = inputFileUri != null ? Uri.base.resolve(inputFileUri) : null; final bool incremental = request[4]; final bool strong = request[5]; final int isolateId = request[6]; final List sourceFiles = request[7]; final bool suppressWarnings = request[8]; final bool syncAsync = request[9]; final String packageConfig = request[10]; Uri platformKernelPath = null; List platformKernel = null; if (request[3] is String) { platformKernelPath = Uri.base.resolveUri(new Uri.file(request[3])); } else if (request[3] is List) { platformKernelPath = Uri.parse(platformKernelFile); platformKernel = request[3]; } else { platformKernelPath = computePlatformBinariesLocation() .resolve(strong ? 'vm_platform_strong.dill' : 'vm_platform.dill'); } Compiler compiler; // Update the in-memory file system with the provided sources. Currently, only // unit tests compile sources that are not on the file system, so this can only // happen during unit tests. if (tag == kUpdateSourcesTag) { assert(incremental, "Incremental compiler required for use of 'kUpdateSourcesTag'"); compiler = lookupIncrementalCompiler(isolateId); assert(compiler != null); updateSources(compiler, sourceFiles); port.send(new CompilationResult.ok(null).toResponse()); return; } else if (tag == kAcceptTag) { assert( incremental, "Incremental compiler required for use of 'kAcceptTag'"); compiler = lookupIncrementalCompiler(isolateId); // There are unit tests that invoke the IncrementalCompiler directly and // request a reload, meaning that we won't have a compiler for this isolate. if (compiler != null) { (compiler as IncrementalCompilerWrapper).accept(); } port.send(new CompilationResult.ok(null).toResponse()); return; } // script should only be null for kUpdateSourcesTag. assert(script != null); // TODO(aam): There should be no need to have an option to choose // one compiler or another. We should always use an incremental // compiler as its functionality is a super set of the other one. We need to // watch the performance though. if (incremental) { compiler = await lookupOrBuildNewIncrementalCompiler( isolateId, sourceFiles, platformKernelPath, platformKernel, strongMode: strong, suppressWarnings: suppressWarnings, syncAsync: syncAsync, packageConfig: packageConfig); } else { final FileSystem fileSystem = sourceFiles.isEmpty && platformKernel == null ? StandardFileSystem.instance : _buildFileSystem(sourceFiles, platformKernel); compiler = new SingleShotCompilerWrapper(fileSystem, platformKernelPath, requireMain: sourceFiles.isEmpty, strongMode: strong, suppressWarnings: suppressWarnings, syncAsync: syncAsync, packageConfig: packageConfig); } CompilationResult result; try { if (verbose) { print("DFE: scriptUri: ${script}"); } Component component = await compiler.compile(script); if (compiler.errors.isNotEmpty) { result = new CompilationResult.errors(compiler.errors, serializeComponent(component, filter: (lib) => !lib.isExternal)); } else { // We serialize the component excluding vm_platform.dill because the VM has // these sources built-in. Everything loaded as a summary in // [kernelForProgram] is marked `external`, so we can use that bit to // decide what to exclude. result = new CompilationResult.ok( serializeComponent(component, filter: (lib) => !lib.isExternal)); } } catch (error, stack) { result = new CompilationResult.crash(error, stack); } if (verbose) print("DFE:> ${result}"); if (tag == kTrainTag) { if (result.status != Status.ok) { tag = -tag; } port.send([tag, inputFileUri, inputFileUri, null, result.payload]); } else if (tag == kCompileTag) { port.send(result.toResponse()); } else { port.send([ -tag, inputFileUri, inputFileUri, null, new CompilationResult.errors(["unknown tag"], null).payload ]); } } /// Creates a file system containing the files specified in [namedSources] and /// that delegates to the underlying file system for any other file request. /// The [namedSources] list interleaves file name string and /// raw file content Uint8List. /// /// The result can be used instead of StandardFileSystem.instance by the /// frontend. FileSystem _buildFileSystem(List namedSources, List platformKernel) { MemoryFileSystem fileSystem = new MemoryFileSystem(Uri.parse('file:///')); if (namedSources != null) { for (int i = 0; i < namedSources.length ~/ 2; i++) { fileSystem .entityForUri(Uri.parse(namedSources[i * 2])) .writeAsBytesSync(namedSources[i * 2 + 1]); } } if (platformKernel != null) { fileSystem .entityForUri(Uri.parse(platformKernelFile)) .writeAsBytesSync(platformKernel); } return new HybridFileSystem(fileSystem); } train(String scriptUri, String platformKernelPath) { // TODO(28532): Enable on Windows. if (Platform.isWindows) return; var tag = kTrainTag; var responsePort = new RawReceivePort(); responsePort.handler = (response) { if (response[0] == tag) { // Success. responsePort.close(); } else if (response[0] == -tag) { // Compilation error. throw response[4]; } else { throw "Unexpected response: $response"; } }; var request = [ tag, responsePort.sendPort, scriptUri, platformKernelPath, false /* incremental */, false /* strong */, 1 /* isolateId chosen randomly */, [] /* source files */, false /* suppress warnings */, false /* synchronous async */, null /* package_config */, ]; _processLoadRequest(request); } main([args]) { if ((args?.length ?? 0) > 1 && args[0] == '--train') { // This entry point is used when creating an app snapshot. The argument // provides a script to compile to warm-up generated code. train(args[1], args.length > 2 ? args[2] : null); } else { // Entry point for the Kernel isolate. return new RawReceivePort()..handler = _processLoadRequest; } } /// Compilation status codes. /// /// Note: The [index] property of these constants must match /// `Dart_KernelCompilationStatus` in /// [dart_api.h](../../../../runtime/include/dart_api.h). enum Status { /// Compilation was successful. ok, /// Compilation failed with a compile time error. error, /// Compiler crashed. crash, } abstract class CompilationResult { CompilationResult._(); factory CompilationResult.ok(Uint8List bytes) = _CompilationOk; factory CompilationResult.errors(List errors, Uint8List bytes) = _CompilationError; factory CompilationResult.crash(Object exception, StackTrace stack) = _CompilationCrash; Status get status; get payload; List toResponse() => [status.index, payload]; } class _CompilationOk extends CompilationResult { final Uint8List bytes; _CompilationOk(this.bytes) : super._(); @override Status get status => Status.ok; @override get payload => bytes; String toString() => "_CompilationOk(${bytes.length} bytes)"; } abstract class _CompilationFail extends CompilationResult { _CompilationFail() : super._(); String get errorString; @override get payload => errorString; } class _CompilationError extends _CompilationFail { final Uint8List bytes; final List errors; _CompilationError(this.errors, this.bytes); @override Status get status => Status.error; @override String get errorString => errors.take(10).join('\n'); String toString() => "_CompilationError(${errorString})"; List toResponse() => [status.index, payload, bytes]; } class _CompilationCrash extends _CompilationFail { final Object exception; final StackTrace stack; _CompilationCrash(this.exception, this.stack); @override Status get status => Status.crash; @override String get errorString => "${exception}\n${stack}"; String toString() => "_CompilationCrash(${errorString})"; } Future runWithPrintToStderr(Future f()) { return runZoned(() => new Future(f), zoneSpecification: new ZoneSpecification( print: (_1, _2, _3, String line) => stderr.writeln(line))); }