blob: 03c94f767d06ebff19ed3fadfa024a8560d76402 [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.
// @dart = 2.9
import 'dart:async';
import 'package:_fe_analyzer_shared/src/messages/diagnostic_message.dart'
show DiagnosticMessage, DiagnosticMessageHandler;
import 'package:_fe_analyzer_shared/src/messages/codes.dart'
show Code, Message, PlainAndColorizedString;
import 'package:dev_compiler/dev_compiler.dart';
import 'package:dev_compiler/src/compiler/js_names.dart' as js_ast;
import 'package:dev_compiler/src/compiler/module_builder.dart';
import 'package:dev_compiler/src/js_ast/js_ast.dart' as js_ast;
import 'package:front_end/src/api_unstable/ddc.dart';
import 'package:kernel/ast.dart'
show
Block,
Class,
Component,
Constructor,
DartType,
Field,
FunctionNode,
Library,
Member,
Node,
Procedure,
RedirectingFactoryConstructor,
TreeNode,
TypeParameter,
VariableDeclaration,
Visitor,
VisitorNullMixin,
VisitorVoidMixin;
DiagnosticMessage _createInternalError(Uri uri, int line, int col, String msg) {
return Message(Code<String>('Expression Compiler Internal error'),
message: msg)
.withLocation(uri, 0, 0)
.withFormatting(PlainAndColorizedString.plainOnly('Internal error: $msg'),
line, col, Severity.internalProblem, []);
}
/// Dart scope
///
/// Provides information about symbols available inside a dart scope.
class DartScope {
final Library library;
final Class cls;
final Member member;
final bool isStatic;
final Map<String, DartType> definitions;
final List<TypeParameter> typeParameters;
DartScope(this.library, this.cls, this.member, this.definitions,
this.typeParameters)
: isStatic = member is Procedure ? member.isStatic : false;
@override
String toString() {
return '''DartScope {
Library: ${library.importUri},
Class: ${cls?.name},
Procedure: $member,
isStatic: $isStatic,
Scope: $definitions,
typeParameters: $typeParameters
}
''';
}
}
/// DartScopeBuilder finds dart scope information for a location.
///
/// Find all definitions in scope at a given 1-based [line] and [column]:
///
/// - library
/// - class
/// - locals
/// - formals
/// - captured variables (for closures)
class DartScopeBuilder extends Visitor<void> with VisitorVoidMixin {
final Component _component;
final int _line;
final int _column;
Library _library;
Class _cls;
Member _member;
int _offset;
DiagnosticMessageHandler onDiagnostic;
final List<FunctionNode> _functions = [];
final Map<String, DartType> _definitions = {};
final List<TypeParameter> _typeParameters = [];
DartScopeBuilder._(this._component, this._line, this._column);
static DartScope findScope(Component component, Library library, int line,
int column, DiagnosticMessageHandler onDiagnostic) {
var builder = DartScopeBuilder._(component, line, column)
..onDiagnostic = onDiagnostic;
library.accept(builder);
return builder.build();
}
DartScope build() {
if (_offset == null || _library == null) return null;
return DartScope(_library, _cls, _member, _definitions, _typeParameters);
}
@override
void defaultTreeNode(Node node) {
node.visitChildren(this);
}
@override
void visitLibrary(Library library) {
_library = library;
_offset = 0;
if (_line > 0) {
_offset = _component.getOffset(_library.fileUri, _line, _column);
}
// Exit early if the evaluation offset is not found.
// Note: the complete scope is not found in this case,
// so the expression compiler will report an error.
if (_offset >= 0) super.visitLibrary(library);
}
@override
void visitClass(Class cls) {
if (_scopeContainsOffset(cls.fileOffset, cls.fileEndOffset, _offset)) {
_cls = cls;
_typeParameters.addAll(cls.typeParameters);
super.visitClass(cls);
}
}
@override
void defaultMember(Member m) {
if (_scopeContainsOffset(m.fileOffset, m.fileEndOffset, _offset)) {
_member = m;
super.defaultMember(m);
}
}
@override
void visitFunctionNode(FunctionNode fun) {
if (_scopeContainsOffset(fun.fileOffset, fun.fileEndOffset, _offset)) {
_functions.add(fun);
_typeParameters.addAll(fun.typeParameters);
super.visitFunctionNode(fun);
}
}
@override
void visitVariableDeclaration(VariableDeclaration decl) {
// Collect locals and formals appearing before current breakpoint.
// Note that we include variables with no offset because the offset
// is not set in many cases in generated code, so omitting them would
// make expression evaluation fail in too many cases.
// Issue: https://github.com/dart-lang/sdk/issues/43966
if (decl.fileOffset < 0 || decl.fileOffset < _offset) {
_definitions[decl.name] = decl.type;
}
super.visitVariableDeclaration(decl);
}
@override
void visitBlock(Block block) {
var fileEndOffset = FileEndOffsetCalculator.calculateEndOffset(block);
if (_scopeContainsOffset(block.fileOffset, fileEndOffset, _offset)) {
super.visitBlock(block);
}
}
bool _scopeContainsOffset(int startOffset, int endOffset, int offset) {
if (offset < 0 || startOffset < 0 || endOffset < 0) {
return false;
}
return startOffset <= offset && offset <= endOffset;
}
}
/// File end offset calculator.
///
/// Helps calculate file end offsets for nodes with internal scope
/// that do not have .fileEndOffset field.
///
/// For example - [Block]
class FileEndOffsetCalculator extends Visitor<int> with VisitorNullMixin<int> {
static const int noOffset = -1;
final int _startOffset;
final TreeNode _root;
int _endOffset = noOffset;
/// Create calculator for a scoping node with no .fileEndOffset.
///
/// [_root] is the parent of the scoping node.
/// [_startOffset] is the start offset of the scoping node.
FileEndOffsetCalculator._(this._root, this._startOffset);
/// Calculate file end offset for a scoping node.
///
/// This calculator finds the first node in the ancestor chain that
/// can give such information for a given [node], i.e. satisfies one
/// of the following conditions:
///
/// - a node with with a greater start offset that is a child of the
/// closest ancestor. The start offset of this child is used as a
/// file end offset of the [node].
///
/// - the closest ancestor with .fileEndOffset information. The file
/// end offset of the ancestor is used as the file end offset of
/// the [node.]
///
/// If none found, return [noOffset].
static int calculateEndOffset(TreeNode node) {
for (var n = node.parent; n != null; n = n.parent) {
var calculator = FileEndOffsetCalculator._(n, node.fileOffset);
var offset = n.accept(calculator);
if (offset != noOffset) return offset;
}
return noOffset;
}
@override
int defaultTreeNode(TreeNode node) {
if (node == _root) {
node.visitChildren(this);
if (_endOffset != noOffset) return _endOffset;
return _endOffsetForNode(node);
}
if (_endOffset == noOffset && node.fileOffset > _startOffset) {
_endOffset = node.fileOffset;
}
return _endOffset;
}
static int _endOffsetForNode(TreeNode node) {
if (node is Class) return node.fileEndOffset;
if (node is Constructor) return node.fileEndOffset;
if (node is Procedure) return node.fileEndOffset;
if (node is Field) return node.fileEndOffset;
if (node is RedirectingFactoryConstructor) return node.fileEndOffset;
if (node is FunctionNode) return node.fileEndOffset;
return noOffset;
}
}
class ExpressionCompiler {
static final String debugProcedureName = '\$dartEval';
final CompilerContext _context;
final CompilerOptions _options;
final List<String> errors;
final IncrementalCompiler _compiler;
final ProgramCompiler _kernel2jsCompiler;
final Component _component;
final ModuleFormat _moduleFormat;
DiagnosticMessageHandler onDiagnostic;
void _log(String message) {
if (_options.verbose) {
_context.options.ticker.logMs(message);
}
}
ExpressionCompiler(
this._options,
this._moduleFormat,
this.errors,
this._compiler,
this._kernel2jsCompiler,
this._component,
) : onDiagnostic = _options.onDiagnostic,
_context = _compiler.context;
/// Compiles [expression] in [libraryUri] at [line]:[column] to JavaScript
/// in [moduleName].
///
/// [line] and [column] are 1-based.
///
/// Values listed in [jsFrameValues] are substituted for their names in the
/// [expression].
///
/// Returns expression compiled to JavaScript or null on error.
/// Errors are reported using onDiagnostic function.
///
/// [jsFrameValues] is a map from js variable name to its primitive value
/// or another variable name, for example
/// { 'x': '1', 'y': 'y', 'o': 'null' }
Future<String> compileExpressionToJs(String libraryUri, int line, int column,
Map<String, String> jsScope, String expression) async {
try {
// 1. find dart scope where debugger is paused
_log('Compiling expression \n$expression');
var dartScope = await _findScopeAt(Uri.parse(libraryUri), line, column);
if (dartScope == null) {
_log('Scope not found at $libraryUri:$line:$column');
return null;
}
_log('DartScope: $dartScope');
// 2. perform necessary variable substitutions
// TODO(annagrin): we only substitute for the same name or a value
// currently, need to extend to cases where js variable names are
// different from dart.
// See [issue 40273](https://github.com/dart-lang/sdk/issues/40273)
// remove undefined js variables (this allows us to get a reference error
// from chrome on evaluation)
dartScope.definitions
.removeWhere((variable, type) => !jsScope.containsKey(variable));
dartScope.typeParameters
.removeWhere((parameter) => !jsScope.containsKey(parameter.name));
// map from values from the stack when available (this allows to evaluate
// captured variables optimized away in chrome)
var localJsScope = [
...dartScope.typeParameters.map((parameter) => jsScope[parameter.name]),
...dartScope.definitions.keys.map((variable) => jsScope[variable])
];
_log('Performed scope substitutions for expression');
// 3. compile dart expression to JS
var jsExpression = await _compileExpression(dartScope, expression);
if (jsExpression == null) {
_log('Failed to compile expression: \n$expression');
return null;
}
// some adjustments to get proper binding to 'this',
// making closure variables available, and catching errors
// TODO(annagrin): make compiler produce correct expression:
// See [issue 40277](https://github.com/dart-lang/sdk/issues/40277)
// - evaluate to an expression in function and class context
// - allow setting values
// See [issue 40273](https://github.com/dart-lang/sdk/issues/40273)
// - bind to proper 'this'
// - map to correct js names for dart symbols
// 4. create call the expression
if (dartScope.cls != null && !dartScope.isStatic) {
// bind to correct 'this' instead of 'globalThis'
jsExpression = '$jsExpression.bind(this)';
}
// 5. wrap in a try/catch to catch errors
var args = localJsScope.join(',\n ');
jsExpression = jsExpression.split('\n').join('\n ');
var callExpression = '\ntry {'
'\n ($jsExpression('
'\n $args'
'\n ))'
'\n} catch (error) {'
'\n error.name + ": " + error.message;'
'\n}';
_log('Compiled expression \n$expression to $callExpression');
return callExpression;
} catch (e, s) {
onDiagnostic(
_createInternalError(Uri.parse(libraryUri), line, column, '$e:$s'));
return null;
}
}
Future<DartScope> _findScopeAt(Uri libraryUri, int line, int column) async {
if (line < 0) {
onDiagnostic(_createInternalError(
libraryUri, line, column, 'Invalid source location'));
return null;
}
var library = await _getLibrary(libraryUri);
if (library == null) {
onDiagnostic(_createInternalError(
libraryUri, line, column, 'Dart library not found for location'));
return null;
}
var scope = DartScopeBuilder.findScope(
_component, library, line, column, onDiagnostic);
if (scope == null) {
onDiagnostic(_createInternalError(
libraryUri, line, column, 'Dart scope not found for location'));
return null;
}
_log('Detected expression compilation scope');
return scope;
}
Future<Library> _getLibrary(Uri libraryUri) async {
return await _compiler.context.runInContext((_) async {
var builder = _compiler.userCode.loader.builders[libraryUri];
if (builder != null) {
var library =
_compiler.userCode.loader.read(libraryUri, -1, accessor: builder);
return library.library;
}
_log('Loaded library for expression');
return null;
});
}
/// Return a JS function that returns the evaluated results when called.
///
/// [scope] current dart scope information.
/// [expression] expression to compile in given [scope].
Future<String> _compileExpression(DartScope scope, String expression) async {
var procedure = await _compiler.compileExpression(
expression,
scope.definitions,
scope.typeParameters,
debugProcedureName,
scope.library.importUri,
scope.cls?.name,
scope.isStatic);
_log('Compiled expression to kernel');
// TODO: make this code clear and assumptions enforceable
// https://github.com/dart-lang/sdk/issues/43273
if (errors.isNotEmpty) {
return null;
}
var imports = <js_ast.ModuleItem>[];
var jsFun = _kernel2jsCompiler.emitFunctionIncremental(imports,
scope.library, scope.cls, procedure.function, '$debugProcedureName');
_log('Generated JavaScript for expression');
// print JS ast to string for evaluation
var context = js_ast.SimpleJavaScriptPrintingContext();
var opts =
js_ast.JavaScriptPrintingOptions(allowKeywordsInProperties: true);
var tree = transformFunctionModuleFormat(imports, jsFun, _moduleFormat);
tree.accept(
js_ast.Printer(opts, context, localNamer: js_ast.TemporaryNamer(tree)));
_log('Added imports and renamed variables for expression');
return context.getText();
}
}