blob: 9cb61dc3545ac0e2ebf839ed13310cd16571339e [file] [log] [blame]
// Copyright (c) 2020, 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 'package:_fe_analyzer_shared/src/messages/diagnostic_message.dart';
import 'package:_fe_analyzer_shared/src/messages/severity.dart';
import 'package:args/args.dart';
import 'package:build_integration/file_system/multi_root.dart';
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_unstable/ddc.dart';
import 'package:kernel/ast.dart' show Component, Library;
import 'package:kernel/target/targets.dart' show TargetFlags;
import 'package:meta/meta.dart';
import 'package:vm/http_filesystem.dart';
import '../compiler/js_names.dart';
import '../compiler/shared_command.dart';
import 'command.dart';
import 'compiler.dart';
import 'expression_compiler.dart';
import 'target.dart';
/// A wrapper around asset server that redirects file read requests
/// to http get requests to the asset server.
class AssetFileSystem extends HttpAwareFileSystem {
final String server;
final String port;
AssetFileSystem(FileSystem original, this.server, this.port)
: super(original);
Uri resourceUri(Uri uri) =>
FileSystemEntity entityForUri(Uri uri) {
if (uri.scheme == 'file') {
return super.entityForUri(uri);
// pass the uri to the asset server in the debugger
return HttpFileSystemEntity(this, resourceUri(uri));
/// The service that handles expression compilation requests from
/// the debugger.
/// See design documentation and discussion in
/// http://go/dart_expression_evaluation_webdev_google3
/// [ExpressionCompilerWorker] listens to input stream of compile expression
/// requests and outputs responses.
/// Debugger can run the service by running the dartdevc main in an isolate,
/// which sets up the request stream and response callback using send ports.
/// Debugger also can pass an asset server's host and port so the service
/// can read dill files from the [AssetFileSystem] that talks to the asset
/// server behind the scenes over http.
/// Protocol:
/// - debugger and dartdevc expression evaluation service perform the initial
/// handshake to establish two-way communication:
/// - debugger creates an isolate using dartdevc's main method with
/// '--experimental-expression-compiler' flag and passes a send port
/// to dartdevc for sending responces from the service to the debugger.
/// - dartdevc creates a new send port to receive requests on, and sends
/// it back to the debugger, for sending requests to the service.
/// - debugger can now send two types of requests to the dartdevc service.
/// The service handles the requests sequentially in a first come, first
/// serve order.
/// - [UpdateDepsRequest]:
/// This request is sent on (re-)build, making the dartdevc load all
/// newly built full kernel files for given modules.
/// - [CompileExpressionRequest]:
/// This request is sent any time an evaluateInFrame request is made to
/// the debugger's VM service at a breakpoint - for example, on typing
/// in an expression evaluation box, on hover over, evaluation of
/// conditional breakpoints, evaluation of expressions in a watch window.
/// - Debugger closes the requests stream on exit, which effectively stops
/// the service
class ExpressionCompilerWorker {
final Stream<Map<String, dynamic>> requestStream;
final void Function(Map<String, dynamic>) sendResponse;
final _libraryForUri = <Uri, Library>{};
final _componentForLibrary = <Library, Component>{};
final _componentForModuleName = <String, Component>{};
final _componentModuleNames = <Component, String>{};
final ProcessedOptions _processedOptions;
final Component _sdkComponent;
ExpressionCompilerWorker._(this._processedOptions, this._sdkComponent,
this.requestStream, this.sendResponse);
static Future<ExpressionCompilerWorker> createFromArgs(
List<String> args, {
Stream<Map<String, dynamic>> requestStream,
void Function(Map<String, dynamic>) sendResponse,
}) {
// We are destructive on `args`, so make a copy.
args = args.toList();
var environmentDefines = parseAndRemoveDeclaredVariables(args);
var parsedArgs = argParser.parse(args);
FileSystem fileSystem = StandardFileSystem.instance;
var multiRoots = (parsedArgs['multi-root'] as Iterable<String>)
var multiRootScheme = parsedArgs['multi-root-scheme'] as String;
if (multiRoots.isNotEmpty) {
fileSystem = MultiRootFileSystem(multiRootScheme, multiRoots, fileSystem);
var assetServerAddress = parsedArgs['asset-server-address'] as String;
if (assetServerAddress != null) {
var assetServerPort = parsedArgs['asset-server-port'] as String;
fileSystem = AssetFileSystem(
fileSystem, assetServerAddress, assetServerPort ?? '8080');
var experimentalFlags = parseExperimentalFlags(
parsedArgs['enable-experiment'] as List<String>),
onError: (e) => throw e);
return create(
_argToUri(parsedArgs['libraries-file'] as String),
packagesFile: _argToUri(parsedArgs['packages-file'] as String),
sdkSummary: _argToUri(parsedArgs['dart-sdk-summary'] as String),
fileSystem: fileSystem,
environmentDefines: environmentDefines,
experimentalFlags: experimentalFlags,
sdkRoot: _argToUri(parsedArgs['sdk-root'] as String),
trackWidgetCreation: parsedArgs['track-widget-creation'] as bool,
soundNullSafety: parsedArgs['sound-null-safety'] as bool,
verbose: parsedArgs['verbose'] as bool,
requestStream: requestStream,
sendResponse: sendResponse,
static List<String> errors = <String>[];
static List<String> warnings = <String>[];
/// Create the worker and load the sdk outlines.
static Future<ExpressionCompilerWorker> create({
@required Uri librariesSpecificationUri,
@required Uri sdkSummary,
@required FileSystem fileSystem,
Uri packagesFile,
Map<String, String> environmentDefines = const {},
Map<ExperimentalFlag, bool> experimentalFlags = const {},
Uri sdkRoot,
bool trackWidgetCreation = false,
bool soundNullSafety = false,
bool verbose = false,
Stream<Map<String, dynamic>> requestStream, // Defaults to read from stdin
void Function(Map<String, dynamic>)
sendResponse, // Defaults to write to stdout
}) async {
var options = CompilerOptions()
..compileSdk = false
..sdkRoot = sdkRoot
..sdkSummary = sdkSummary
..packagesFileUri = packagesFile
..librariesSpecificationUri = librariesSpecificationUri = DevCompilerTarget(
TargetFlags(trackWidgetCreation: trackWidgetCreation))
..fileSystem = fileSystem
..omitPlatform = true
..environmentDefines = environmentDefines
..experimentalFlags = experimentalFlags
..onDiagnostic = _onDiagnosticHandler(errors, warnings)
..nnbdMode = soundNullSafety ? NnbdMode.Strong : NnbdMode.Weak
..verbose = verbose;
requestStream ??= stdin
.cast<Map<String, dynamic>>();
sendResponse ??= (Map<String, dynamic> response) =>
var processedOpts = ProcessedOptions(options: options);
var sdkComponent = await CompilerContext(processedOpts)
.runInContext<Component>((CompilerContext c) async {
return processedOpts.loadSdkSummary(null);
return ExpressionCompilerWorker._(
processedOpts, sdkComponent, requestStream, sendResponse)
.._update(sdkComponent, dartSdkModule);
/// Starts listening and responding to commands.
/// Completes when the [requestStream] closes and we finish handling the
/// requests.
Future<void> start() async {
await for (var request in requestStream) {
try {
var command = request['command'] as String;
switch (command) {
case 'UpdateDeps':
await _updateDeps(UpdateDepsRequest.fromJson(request)));
case 'CompileExpression':
sendResponse(await _compileExpression(
throw ArgumentError(
'Unrecognized command `$command`, full request was `$request`');
} catch (e, s) {
'exception': '$e',
'stackTrace': '$s',
'succeeded': false,
/// Handles a `CompileExpression` request.
Future<Map<String, dynamic>> _compileExpression(
CompileExpressionRequest request) async {
var libraryUri = Uri.parse(request.libraryUri);
if (libraryUri.scheme == 'dart') {
// compiling expressions inside the SDK currently fails because
// SDK kernel outlines do not contain information that is needed
// to detect the scope for expression evaluation - such as local
// symbols and source file line starts.
throw Exception('Expression compilation inside SDK is not supported yet');
var originalComponent = _componentForModuleName[request.moduleName];
if (originalComponent == null) {
throw ArgumentError(
'Unable to find library `$libraryUri`, it must be loaded first.');
var component = _sdkComponent;
if (libraryUri.scheme != 'dart') {
var libraries =
_collectTransitiveDependencies(originalComponent, _sdkComponent);
component = Component(
libraries: libraries,
nameRoot: originalComponent.root,
uriToSource: originalComponent.uriToSource,
var incrementalCompiler = IncrementalCompiler.forExpressionCompilationOnly(
CompilerContext(_processedOptions), component);
var finalComponent =
await incrementalCompiler.computeDelta(entryPoints: [libraryUri]);
if (errors.isNotEmpty) {
return {
'errors': errors,
'warnings': warnings,
'compiledProcedure': null,
'succeeded': errors.isEmpty,
var compiler = ProgramCompiler(
sourceMap: true, summarizeApi: false, moduleName: request.moduleName),
coreTypes: incrementalCompiler.getCoreTypes(),
var expressionCompiler = ExpressionCompiler(
verbose: _processedOptions.verbose,
onDiagnostic: _onDiagnosticHandler(errors, warnings),
errors: errors,
var compiledProcedure = await expressionCompiler.compileExpressionToJs(
return {
'errors': errors,
'warnings': warnings,
'compiledProcedure': compiledProcedure,
'succeeded': errors.isEmpty,
List<Library> _collectTransitiveDependencies(
Component component, Component sdk) {
var libraries = <Library>{};
var toVisit = <Library>[];
while (toVisit.isNotEmpty) {
var lib = toVisit.removeLast();
if (!libraries.contains(lib)) {
for (var dep in lib.dependencies) {
var uri = dep.importedLibraryReference.asLibrary.importUri;
var library = _libraryForUri[uri];
assert(library == dep.importedLibraryReference.asLibrary);
return libraries.toList();
/// Loads in the specified dill files and invalidates any existing ones.
Future<Map<String, dynamic>> _updateDeps(UpdateDepsRequest request) async {
for (var input in request.inputs) {
var file =
var bytes = await file.readAsBytes();
var component = await _processedOptions.loadComponent(
bytes, _sdkComponent.root,
alwaysCreateNewNamedNodes: true);
_update(component, input.moduleName);
return {'succeeded': true};
void _update(Component component, String moduleName) {
// do not update dart sdk
if (moduleName == dartSdkModule &&
_componentForModuleName.containsKey(moduleName)) {
// cleanup old components and libraries
if (_componentForModuleName.containsKey(moduleName)) {
var oldComponent = _componentForModuleName[moduleName];
for (var lib in oldComponent.libraries) {
// add new components and libraries
_componentModuleNames[component] = moduleName;
_componentForModuleName[moduleName] = component;
for (var lib in component.libraries) {
_componentForLibrary[lib] = component;
_libraryForUri[lib.importUri] = lib;
class CompileExpressionRequest {
final int column;
final String expression;
final Map<String, String> jsModules;
final Map<String, String> jsScope;
final String libraryUri;
final int line;
final String moduleName;
@required this.expression,
@required this.column,
@required this.jsModules,
@required this.jsScope,
@required this.libraryUri,
@required this.line,
@required this.moduleName,
factory CompileExpressionRequest.fromJson(Map<String, dynamic> json) =>
expression: json['expression'] as String,
line: json['line'] as int,
column: json['column'] as int,
jsModules: Map<String, String>.from(json['jsModules'] as Map),
jsScope: Map<String, String>.from(json['jsScope'] as Map),
libraryUri: json['libraryUri'] as String,
moduleName: json['moduleName'] as String,
class UpdateDepsRequest {
final List<InputDill> inputs;
factory UpdateDepsRequest.fromJson(Map<String, dynamic> json) =>
for (var input in json['inputs'] as List)
InputDill(input['path'] as String, input['moduleName'] as String),
class InputDill {
final String moduleName;
final String path;
InputDill(this.path, this.moduleName);
void Function(DiagnosticMessage) _onDiagnosticHandler(
List<String> errors, List<String> warnings) =>
(DiagnosticMessage message) {
switch (message.severity) {
case Severity.error:
case Severity.internalProblem:
case Severity.warning:
case Severity.context:
case Severity.ignored:
throw 'Unexpected severity: ${message.severity}';
final argParser = ArgParser()
help: 'Enable a language experiment when invoking the CFE.')
..addOption('multi-root-scheme', defaultsTo: 'org-dartlang-app')
..addFlag('track-widget-creation', defaultsTo: false)
..addFlag('sound-null-safety', defaultsTo: false)
..addFlag('verbose', defaultsTo: false);
Uri _argToUri(String uriArg) =>
uriArg == null ? null : Uri.base.resolve(uriArg.replaceAll('\\', '/'));