Refined scope calculation in expression compiler
- make sure nested blocks scopes are visited
- make sure variable definitions do not leak beyond block scopes
- properly collect scopes for loops, if statements, constructors
- add calculation of fileEndOffsets for blocks
- save block file offsets to dill
- update binary format version
- change kernel readers and writers to read and write block offsets
- change vm readers to read and block offsets for new version
- add missing fileOffsets and fileEndOffsets on functions for
late fields
- add missing fileOffsets and fileEndOffsets on functions for
extensions
- add errors on failures to find scope
- find libraries for private fields correctly
- add more expression compilation tests
- add test to verify fileOffsets and fileEndOffsets are set for
SDK summary (will add full dill tests later)
Closes: https://github.com/dart-lang/sdk/issues/40278
Related: https://github.com/dart-lang/sdk/issues/34942
Change-Id: I5bc1bb645543045b689d8d61069ee77dc4ee9025
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/167541
Commit-Queue: Anna Gringauze <annagrin@google.com>
Reviewed-by: Nicholas Shahan <nshahan@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart b/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart
index 52b7f91..98448cc 100644
--- a/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart
@@ -4608,6 +4608,58 @@
message: r"""This is the type variable whose bound isn't conformed to.""");
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const Template<
+ Message Function(
+ String
+ string)> templateIncrementalCompilerIllegalParameter = const Template<
+ Message Function(String string)>(
+ messageTemplate:
+ r"""Illegal parameter name '#string' found during expression compilation.""",
+ withArguments: _withArgumentsIncrementalCompilerIllegalParameter);
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const Code<Message Function(String string)>
+ codeIncrementalCompilerIllegalParameter =
+ const Code<Message Function(String string)>(
+ "IncrementalCompilerIllegalParameter",
+ templateIncrementalCompilerIllegalParameter,
+);
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+Message _withArgumentsIncrementalCompilerIllegalParameter(String string) {
+ if (string.isEmpty) throw 'No string provided';
+ return new Message(codeIncrementalCompilerIllegalParameter,
+ message:
+ """Illegal parameter name '${string}' found during expression compilation.""",
+ arguments: {'string': string});
+}
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const Template<Message Function(String string)>
+ templateIncrementalCompilerIllegalTypeParameter =
+ const Template<Message Function(String string)>(
+ messageTemplate:
+ r"""Illegal type parameter name '#string' found during expression compilation.""",
+ withArguments: _withArgumentsIncrementalCompilerIllegalTypeParameter);
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const Code<Message Function(String string)>
+ codeIncrementalCompilerIllegalTypeParameter =
+ const Code<Message Function(String string)>(
+ "IncrementalCompilerIllegalTypeParameter",
+ templateIncrementalCompilerIllegalTypeParameter,
+);
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+Message _withArgumentsIncrementalCompilerIllegalTypeParameter(String string) {
+ if (string.isEmpty) throw 'No string provided';
+ return new Message(codeIncrementalCompilerIllegalTypeParameter,
+ message:
+ """Illegal type parameter name '${string}' found during expression compilation.""",
+ arguments: {'string': string});
+}
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
const Template<Message Function(Uri uri_)> templateInferredPackageUri =
const Template<Message Function(Uri uri_)>(
messageTemplate: r"""Interpreting this as package URI, '#uri'.""",
diff --git a/pkg/compiler/test/sourcemaps/stacktrace/sync_throw_in_async.dart b/pkg/compiler/test/sourcemaps/stacktrace/sync_throw_in_async.dart
index f89ba6d..4a615c8 100644
--- a/pkg/compiler/test/sourcemaps/stacktrace/sync_throw_in_async.dart
+++ b/pkg/compiler/test/sourcemaps/stacktrace/sync_throw_in_async.dart
@@ -9,8 +9,7 @@
/*1:main*/ test();
}
-// TODO(34942): Step 2 should point to the body block.
@pragma('dart2js:noInline')
-test /*2:test*/ () async {
+test() async /*2:test*/ {
/*4:test*/ throw '>ExceptionMarker<';
}
diff --git a/pkg/compiler/test/sourcemaps/stacktrace/sync_throw_in_awaited_async.dart b/pkg/compiler/test/sourcemaps/stacktrace/sync_throw_in_awaited_async.dart
index 8d1ae16..01f5d88 100644
--- a/pkg/compiler/test/sourcemaps/stacktrace/sync_throw_in_awaited_async.dart
+++ b/pkg/compiler/test/sourcemaps/stacktrace/sync_throw_in_awaited_async.dart
@@ -9,15 +9,13 @@
/*1:main*/ test1();
}
-// TODO(34942): Step 3 should point to the body block.
@pragma('dart2js:noInline')
-test1 /*3:test1*/ () async {
+test1() async /*3:test1*/ {
// This call is on the stack when the error is thrown.
await /*5:test1*/ test2();
}
-// TODO(34942): Step 7 should point to the body block.
@pragma('dart2js:noInline')
-test2 /*7:test2*/ () async {
+test2() async /*7:test2*/ {
/*9:test2*/ throw '>ExceptionMarker<';
}
diff --git a/pkg/compiler/test/sourcemaps/stacktrace/sync_throw_in_top_level_method_from_async.dart b/pkg/compiler/test/sourcemaps/stacktrace/sync_throw_in_top_level_method_from_async.dart
index 23b08e9..9475ce8 100644
--- a/pkg/compiler/test/sourcemaps/stacktrace/sync_throw_in_top_level_method_from_async.dart
+++ b/pkg/compiler/test/sourcemaps/stacktrace/sync_throw_in_top_level_method_from_async.dart
@@ -8,9 +8,8 @@
/*1:main*/ test1();
}
-// TODO(34942): Step 2 should point to the body block.
@pragma('dart2js:noInline')
-test1 /*2:test1*/ () async {
+test1() async /*2:test1*/ {
/*9:test1*/ test2();
}
diff --git a/pkg/dev_compiler/lib/src/kernel/expression_compiler.dart b/pkg/dev_compiler/lib/src/kernel/expression_compiler.dart
index 3af0ea9..9d8b566 100644
--- a/pkg/dev_compiler/lib/src/kernel/expression_compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/expression_compiler.dart
@@ -23,38 +23,52 @@
Block,
Class,
Component,
+ Constructor,
DartType,
Field,
FunctionNode,
Library,
+ Member,
Node,
Procedure,
PropertyGet,
PropertySet,
+ RedirectingFactoryConstructor,
+ TreeNode,
TypeParameter,
VariableDeclaration,
Visitor;
+DiagnosticMessage _createInternalError(Uri uri, int line, int col, String msg) {
+ return Message(Code<String>('Expression Compiler Internal error', null),
+ message: msg)
+ .withLocation(uri, 0, 0)
+ .withFormatting(
+ '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 Procedure procedure;
+ final Member member;
+ final bool isStatic;
final Map<String, DartType> definitions;
final List<TypeParameter> typeParameters;
- DartScope(this.library, this.cls, this.procedure, this.definitions,
- this.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: $procedure,
- isStatic: ${procedure.isStatic},
+ Procedure: $member,
+ isStatic: $isStatic,
Scope: $definitions,
typeParameters: $typeParameters
}
@@ -62,34 +76,49 @@
}
}
-/// DartScopeBuilder finds dart scope information in
-/// [component] on a given 1-based [line]:
-/// library, class, locals, formals, and any other
-/// avaiable symbols at that location.
-/// TODO(annagrin): Refine scope detection
-/// See [issue 40278](https://github.com/dart-lang/sdk/issues/40278)
+/// 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> {
final Component _component;
- Library _library;
- Class _cls;
- Procedure _procedure;
- final List<FunctionNode> _functions = [];
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);
+ 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 (_library == null || _procedure == null) return null;
+ if (_offset == null || _library == null || _member == null) return null;
- return DartScope(_library, _cls, _procedure, _definitions, _typeParameters);
+ return DartScope(_library, _cls, _member, _definitions, _typeParameters);
}
@override
- void defaultNode(Node node) {
+ void defaultTreeNode(Node node) {
node.visitChildren(this);
}
@@ -98,7 +127,10 @@
_library = library;
_offset = _component.getOffset(_library.fileUri, _line, _column);
- super.visitLibrary(library);
+ // 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
@@ -112,61 +144,115 @@
}
@override
- void visitProcedure(Procedure p) {
- if (_scopeContainsOffset(p.fileOffset, p.fileEndOffset, _offset)) {
- _procedure = p;
+ void defaultMember(Member m) {
+ if (_scopeContainsOffset(m.fileOffset, m.fileEndOffset, _offset)) {
+ _member = m;
- super.visitProcedure(p);
+ super.defaultMember(m);
}
}
@override
void visitFunctionNode(FunctionNode fun) {
if (_scopeContainsOffset(fun.fileOffset, fun.fileEndOffset, _offset)) {
- _collectDefinitions(fun);
+ _functions.add(fun);
_typeParameters.addAll(fun.typeParameters);
super.visitFunctionNode(fun);
}
}
- void _collectDefinitions(FunctionNode fun) {
- _functions.add(fun);
+ @override
+ void visitVariableDeclaration(VariableDeclaration decl) {
+ // collect locals and formals
+ _definitions[decl.name] = decl.type;
+ super.visitVariableDeclaration(decl);
+ }
- // add formals
- for (var formal in fun.namedParameters) {
- _definitions[formal.name] = formal.type;
- }
-
- for (var formal in fun.positionalParameters) {
- _definitions[formal.name] = formal.type;
- }
-
- // add locals
- var body = fun.body;
- if (body is VariableDeclaration) {
- // local
- _definitions[body.name] = body.type;
- }
- if (body is Block) {
- for (var stmt in body.statements) {
- if (stmt is VariableDeclaration) {
- // local
- _definitions[stmt.name] = stmt.type;
- }
- }
+ @override
+ void visitBlock(Block block) {
+ var fileEndOffset = FileEndOffsetCalculator.calculateEndOffset(block);
+ if (_scopeContainsOffset(block.fileOffset, fileEndOffset, _offset)) {
+ super.visitBlock(block);
}
}
- static bool _scopeContainsOffset(int startOffset, int endOffset, int offset) {
- if (offset < 0) return false;
- if (startOffset < 0) return false;
- if (endOffset < 0) return false;
-
+ 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> {
+ 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 PrivateFieldsVisitor extends Visitor<void> {
final Map<String, String> privateFields = {};
@@ -178,28 +264,32 @@
@override
void visitFieldReference(Field node) {
if (node.name.isPrivate) {
- privateFields[node.name.text] = node.name.library.importUri.toString();
+ var library = node.enclosingLibrary?.importUri;
+ privateFields[node.name.text] = library?.toString();
}
}
@override
void visitField(Field node) {
if (node.name.isPrivate) {
- privateFields[node.name.text] = node.name.library.importUri.toString();
+ var library = node.enclosingLibrary?.importUri;
+ privateFields[node.name.text] = library?.toString();
}
}
@override
void visitPropertyGet(PropertyGet node) {
if (node.name.isPrivate) {
- privateFields[node.name.text] = node.name.library.importUri.toString();
+ var library = node.interfaceTarget?.enclosingLibrary?.importUri;
+ privateFields[node.name.text] = library?.toString();
}
}
@override
void visitPropertySet(PropertySet node) {
if (node.name.isPrivate) {
- privateFields[node.name.text] = node.name.library.importUri.toString();
+ var library = node.interfaceTarget?.enclosingLibrary?.importUri;
+ privateFields[node.name.text] = library?.toString();
}
}
}
@@ -260,83 +350,89 @@
Map<String, String> jsScope,
String moduleName,
String expression) async {
- // 1. find dart scope where debugger is paused
+ try {
+ // 1. find dart scope where debugger is paused
- _log('Compiling expression in $moduleName:\n$expression');
+ _log('Compiling expression in $moduleName:\n$expression');
- var dartScope = await _findScopeAt(Uri.parse(libraryUri), line, column);
- if (dartScope == null) {
- _log('Scope not found at $libraryUri:$line:$column');
+ var dartScope = await _findScopeAt(Uri.parse(libraryUri), line, column);
+ if (dartScope == null) {
+ _log('Scope not found at $libraryUri:$line:$column');
+ return null;
+ }
+
+ // 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));
+
+ // map from values from the stack when available (this allows to evaluate
+ // captured variables optimized away in chrome)
+ var localJsScope =
+ dartScope.definitions.keys.map((variable) => jsScope[variable]);
+
+ _log('Performed scope substitutions for expression');
+
+ // 3. compile dart expression to JS
+
+ var jsExpression = await _compileExpression(
+ dartScope, jsModules, moduleName, expression);
+
+ if (jsExpression == null) {
+ _log('Failed to compile expression in $moduleName:\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;
}
-
- // 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));
-
- // map from values from the stack when available (this allows to evaluate
- // captured variables optimized away in chrome)
- var localJsScope =
- dartScope.definitions.keys.map((variable) => jsScope[variable]);
-
- _log('Performed scope substitutions for expression');
-
- // 3. compile dart expression to JS
-
- var jsExpression =
- await _compileExpression(dartScope, jsModules, moduleName, expression);
-
- if (jsExpression == null) {
- _log('Failed to compile expression in $moduleName:\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.procedure.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(', ');
- var callExpression = '''
-try {
-($jsExpression(
-$args
-))
-} catch (error) {
-error.name + ": " + error.message;
-}''';
-
- _log(
- 'Compiled expression in $moduleName:\n$expression to \n$callExpression');
- return callExpression;
}
Future<DartScope> _findScopeAt(Uri libraryUri, int line, int column) async {
if (line < 0) {
onDiagnostic(_createInternalError(
- libraryUri, line, column, 'invalid source location: $line, $column'));
+ libraryUri, line, column, 'Invalid source location'));
return null;
}
@@ -347,9 +443,8 @@
return null;
}
- var builder = DartScopeBuilder(_component, line, column);
- library.accept(builder);
- var scope = builder.build();
+ var scope = DartScopeBuilder.findScope(
+ _component, library, line, column, onDiagnostic);
if (scope == null) {
onDiagnostic(_createInternalError(
libraryUri, line, column, 'Dart scope not found for location'));
@@ -375,40 +470,6 @@
});
}
- /// Creates a stament to require a module to bring it back to scope
- /// example:
- /// let dart = require('dart_sdk').dart;
- js_ast.Statement _createRequireModuleStatement(
- String moduleName, String moduleVariable, String fieldName) {
- var variableName = moduleVariable.replaceFirst('.dart', '');
- var rhs = js_ast.PropertyAccess.field(
- js_ast.Call(js_ast.Identifier('require'),
- [js_ast.LiteralExpression('\'$moduleName\'')]),
- '$fieldName');
-
- return rhs.toVariableDeclaration(js_ast.Identifier('$variableName'));
- }
-
- js_ast.Statement _createPrivateField(String field, String library) {
- var libraryName = library.replaceFirst('.dart', '');
- var rhs = js_ast.Call(
- js_ast.PropertyAccess.field(js_ast.Identifier('dart'), 'privateName'), [
- js_ast.LiteralExpression(libraryName),
- js_ast.LiteralExpression('"$field"')
- ]);
-
- // example:
- // let _f = dart.privateName(main, "_f");
- return rhs.toVariableDeclaration(js_ast.Identifier('$field'));
- }
-
- DiagnosticMessage _createInternalError(
- Uri uri, int line, int col, String msg) {
- return Message(Code<String>('Internal error', null), message: msg)
- .withLocation(uri, 0, 0)
- .withFormatting('', line, col, Severity.internalProblem, []);
- }
-
/// Return a JS function that returns the evaluated results when called.
///
/// [scope] current dart scope information
@@ -431,7 +492,7 @@
debugProcedureName,
scope.library.importUri,
scope.cls?.name,
- scope.procedure.isStatic);
+ scope.isStatic);
_log('Compiled expression to kernel');
@@ -452,9 +513,29 @@
_log('Generated JavaScript for expression');
- // apply temporary workarounds for what ideally
- // needs to be done in the compiler
+ var jsFunModified = _addSymbolDefinitions(procedure, jsFun, scope, modules);
+ _log('Added symbol definitions to JavaScript');
+
+ // print JS ast to string for evaluation
+
+ var context = js_ast.SimpleJavaScriptPrintingContext();
+ var opts =
+ js_ast.JavaScriptPrintingOptions(allowKeywordsInProperties: true);
+
+ jsFunModified.accept(js_ast.Printer(opts, context));
+ _log('Performed JavaScript adjustments for expression');
+
+ return context.getText();
+ }
+
+ /// Add symbol definitions for all symbols in compiled expression
+ ///
+ /// TODO: this is a temporary workaround to make JavaScript produced
+ /// by the ProgramCompiler self-contained.
+ /// Issue: https://github.com/dart-lang/sdk/issues/41480
+ js_ast.Fun _addSymbolDefinitions(Procedure procedure, js_ast.Fun jsFun,
+ DartScope scope, Map<String, String> modules) {
// get private fields accessed by the evaluated expression
var fieldsCollector = PrivateFieldsVisitor();
procedure.accept(fieldsCollector);
@@ -467,20 +548,22 @@
var module = modules[variable];
for (var field in privateFields.keys) {
var library = privateFields[field];
- var libraryVariable =
- library.replaceAll('.dart', '').replaceAll('/', '__');
- if (libraryVariable.endsWith(variable)) {
- if (currentLibraries[field] != null) {
- onDiagnostic(_createInternalError(
- scope.library.importUri,
- 0,
- 0,
- 'ExpressionCompiler: $field defined in more than one library: '
- '${currentLibraries[field]}, $variable'));
- return null;
+ if (library != null) {
+ var libraryVariable =
+ library.replaceAll('.dart', '').replaceAll('/', '__');
+ if (libraryVariable.endsWith(variable)) {
+ if (currentLibraries[field] != null) {
+ onDiagnostic(_createInternalError(
+ scope.library.importUri,
+ 0,
+ 0,
+ 'ExpressionCompiler: $field defined in more than one library: '
+ '${currentLibraries[field]}, $variable'));
+ return null;
+ }
+ currentLibraries[field] = variable;
+ currentModules[variable] = module;
}
- currentLibraries[field] = variable;
- currentModules[variable] = module;
}
}
}
@@ -497,17 +580,36 @@
...jsFun.body.statements
]);
- var jsFunModified = js_ast.Fun(jsFun.params, body);
+ return js_ast.Fun(jsFun.params, body);
+ }
- // print JS ast to string for evaluation
+ /// Creates a library symbol definition
+ ///
+ /// example:
+ /// let dart = require('dart_sdk').dart;
+ js_ast.Statement _createRequireModuleStatement(
+ String moduleName, String moduleVariable, String fieldName) {
+ var variableName = moduleVariable.replaceFirst('.dart', '');
+ var rhs = js_ast.PropertyAccess.field(
+ js_ast.Call(js_ast.Identifier('require'),
+ [js_ast.LiteralExpression('\'$moduleName\'')]),
+ '$fieldName');
- var context = js_ast.SimpleJavaScriptPrintingContext();
- var opts =
- js_ast.JavaScriptPrintingOptions(allowKeywordsInProperties: true);
+ return rhs.toVariableDeclaration(js_ast.Identifier('$variableName'));
+ }
- jsFunModified.accept(js_ast.Printer(opts, context));
- _log('Performed JavaScript adjustments for expression');
+ /// Creates a private symbol definition
+ ///
+ /// example:
+ /// let _f = dart.privateName(main, "_f");
+ js_ast.Statement _createPrivateField(String field, String library) {
+ var libraryName = library.replaceFirst('.dart', '');
+ var rhs = js_ast.Call(
+ js_ast.PropertyAccess.field(js_ast.Identifier('dart'), 'privateName'), [
+ js_ast.LiteralExpression(libraryName),
+ js_ast.LiteralExpression('"$field"')
+ ]);
- return context.getText();
+ return rhs.toVariableDeclaration(js_ast.Identifier('$field'));
}
}
diff --git a/pkg/dev_compiler/test/expression_compiler/expression_compiler_test.dart b/pkg/dev_compiler/test/expression_compiler/expression_compiler_test.dart
index 5636b5f..f1b9e63 100644
--- a/pkg/dev_compiler/test/expression_compiler/expression_compiler_test.dart
+++ b/pkg/dev_compiler/test/expression_compiler/expression_compiler_test.dart
@@ -222,13 +222,14 @@
scope: scope,
expression: expression);
- if (expectedError != null) {
- expect(result.isSuccess, isFalse);
- expect(_normalize(result.result), matches(expectedError));
- } else if (expectedResult != null) {
- expect(result.isSuccess, isTrue);
- expect(_normalize(result.result), _matches(expectedResult));
- }
+ var success = expectedError == null;
+ var message = success ? expectedResult : expectedError;
+
+ expect(
+ result,
+ const TypeMatcher<TestCompilationResult>()
+ .having((r) => _normalize(r.result), 'result', _matches(message))
+ .having((r) => r.isSuccess, 'isSuccess', success));
}
String _normalize(String text) {
@@ -236,11 +237,8 @@
}
Matcher _matches(String text) {
- var indent = text.indexOf(RegExp('[^ ]'));
- var unindented =
- text.split('\n').map((line) => line.substring(indent)).join('\n');
-
- return matches(RegExp(RegExp.escape(unindented), multiLine: true));
+ var unindented = RegExp.escape(text).replaceAll(RegExp('[ ]+'), '[ ]*');
+ return matches(RegExp(unindented, multiLine: true));
}
int _getEvaluationLine(String source) {
@@ -290,7 +288,14 @@
expectedError: "Error: Getter not found: 'typo'");
});
- test('local', () async {
+ test('local (trimmed scope)', () async {
+ // Test that current expression evaluation works in extension methods.
+ //
+ // Note: the actual scope is {#this, ret}, but #this is effectively
+ // removed in the expression compilator because it does not exist
+ // in JavaScript code.
+ // See (full scope) tests for what will the evaluation will look like
+ // when the mapping from dart symbols to JavaScipt symbols is added.
await driver.check(
scope: <String, String>{'ret': '1234'},
expression: 'ret',
@@ -298,16 +303,110 @@
(function(ret) {
return ret;
}(
- 1234
+ 1234
))
''');
});
- test('this', () async {
+ test('local (full scope)', () async {
+ // Test evalution in extension methods in the future when the mapping
+ // from kernel symbols to dartdevc symbols is added.
+ //
+ // Note: this currently fails due to
+ // - incremental compiler not allowing #this as a parameter name
await driver.check(
- scope: <String, String>{'ret': '1234'},
+ scope: <String, String>{'ret': '1234', '#this': 'this'},
+ expression: 'ret',
+ expectedError:
+ "Illegal parameter name '#this' found during expression compilation.");
+ });
+
+ test('this (full scope)', () async {
+ // Test evalution in extension methods in the future when the mapping
+ // from kernel symbols to dartdevc symbols is added.
+ //
+ // Note: this currently fails due to
+ // - incremental compiler not allowing #this as a parameter name
+ // - incremental compiler not mapping 'this' from user input to '#this'
+ await driver.check(
+ scope: <String, String>{'ret': '1234', '#this': 'this'},
expression: 'this',
- expectedError: "Expected identifier, but got 'this'.");
+ expectedError:
+ "Illegal parameter name '#this' found during expression compilation.");
+ });
+ });
+
+ group('Expression compiler tests in static function:', () {
+ const source = '''
+ int foo(int x, {int y}) {
+ int z = 0;
+ /* evaluation placeholder */
+ return x + y + z;
+ }
+
+ main() => 0;
+ ''';
+
+ TestDriver driver;
+
+ setUp(() {
+ driver = TestDriver(options, source);
+ });
+
+ tearDown(() {
+ driver.delete();
+ });
+
+ test('compilation error', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1', 'y': '2', 'z': '3'},
+ expression: 'typo',
+ expectedError: "Getter not found: \'typo\'");
+ });
+
+ test('local', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1', 'y': '2', 'z': '3'},
+ expression: 'x',
+ expectedResult: '''
+ (function(x, y, z) {
+ return x;
+ }(
+ 1,
+ 2,
+ 3
+ ))
+ ''');
+ });
+
+ test('formal', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1', 'y': '2', 'z': '3'},
+ expression: 'y',
+ expectedResult: '''
+ (function(x, y, z) {
+ return y;
+ }(
+ 1,
+ 2,
+ 3
+ ))
+ ''');
+ });
+
+ test('named formal', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1', 'y': '2', 'z': '3'},
+ expression: 'z',
+ expectedResult: '''
+ (function(x, y, z) {
+ return z;
+ }(
+ 1,
+ 2,
+ 3
+ ))
+ ''');
});
});
@@ -369,7 +468,7 @@
(function(x) {
return x;
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -382,7 +481,7 @@
(function(x) {
return this;
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -395,7 +494,7 @@
(function(x) {
return dart.notNull(x) + 1;
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -408,7 +507,7 @@
(function(x) {
return dart.notNull(x) + dart.notNull(foo.C.staticField);
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -423,7 +522,7 @@
let _staticField = dart.privateName(foo, "_staticField");
return dart.notNull(x) + dart.notNull(foo.C._staticField);
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -436,7 +535,7 @@
(function(x) {
return dart.notNull(x) + dart.notNull(this.field);
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -451,7 +550,7 @@
let _field = dart.privateName(foo, "_field");
return dart.notNull(x) + dart.notNull(this[_field]);
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -464,7 +563,7 @@
(function(x) {
return dart.notNull(x) + dart.notNull(foo.global);
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -477,7 +576,7 @@
(function(x) {
return this.methodFieldAccess(2);
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -490,7 +589,7 @@
(function(x) {
return this.asyncMethod(2);
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -500,12 +599,12 @@
scope: <String, String>{'x': '1'},
expression: '"1234".parseInt()',
expectedResult: '''
- (function(x) {
- return foo['NumberParsing|parseInt']("1234");
- }.bind(this)(
- 1
- ))
- ''');
+ (function(x) {
+ return foo['NumberParsing|parseInt']("1234");
+ }.bind(this)(
+ 1
+ ))
+ ''');
});
test('private field modification', () async {
@@ -513,14 +612,14 @@
scope: <String, String>{'x': '1'},
expression: '_field = 2',
expectedResult: '''
- (function(x) {
- let foo = require('foo.dart').foo;
- let _field = dart.privateName(foo, "_field");
- return this[_field] = 2;
- }.bind(this)(
- 1
- ))
- ''');
+ (function(x) {
+ let foo = require('foo.dart').foo;
+ let _field = dart.privateName(foo, "_field");
+ return this[_field] = 2;
+ }.bind(this)(
+ 1
+ ))
+ ''');
});
test('field modification', () async {
@@ -531,7 +630,7 @@
(function(x) {
return this.field = 2;
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -546,7 +645,7 @@
let _staticField = dart.privateName(foo, "_staticField");
return foo.C._staticField = 2;
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -559,7 +658,7 @@
(function(x) {
return foo.C.staticField = 2;
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -622,7 +721,7 @@
(function(x) {
return dart.notNull(x) + dart.notNull(foo.C.staticField);
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -637,7 +736,7 @@
let _staticField = dart.privateName(foo, "_staticField");
return dart.notNull(x) + dart.notNull(foo.C._staticField);
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -650,7 +749,7 @@
(function(x) {
return dart.notNull(x) + dart.notNull(this.field);
}.bind(this)(
- 1
+ 1
))
''');
});
@@ -852,7 +951,8 @@
(function(x, c) {
return x;
}(
- 1, null
+ 1,
+ null
))
''');
});
@@ -865,7 +965,8 @@
(function(x, c) {
return c;
}(
- 1, null
+ 1,
+ null
))
''');
});
@@ -878,7 +979,8 @@
(function(x, c) {
return new foo.C.new(1, 3);
}(
- 1, null
+ 1,
+ null
))
''');
});
@@ -893,7 +995,8 @@
let _field = dart.privateName(foo, "_field");
return new foo.C.new(1, 3)[_field];
}(
- 1, null
+ 1,
+ null
))
''');
});
@@ -906,7 +1009,8 @@
(function(x, c) {
return foo.C.staticField;
}(
- 1, null
+ 1,
+ null
))
''');
});
@@ -926,7 +1030,8 @@
(function(x, c) {
return c.field;
}(
- 1, null
+ 1,
+ null
))
''');
});
@@ -941,7 +1046,8 @@
let _field = dart.privateName(foo, "_field");
return c[_field];
}(
- 1, null
+ 1,
+ null
))
''');
});
@@ -954,7 +1060,8 @@
(function(x, c) {
return c.methodFieldAccess(2);
}(
- 1, null
+ 1,
+ null
))
''');
});
@@ -967,7 +1074,8 @@
(function(x, c) {
return c.asyncMethod(2);
}(
- 1, null
+ 1,
+ null
))
''');
});
@@ -977,12 +1085,13 @@
scope: <String, String>{'x': '1', 'c': 'null'},
expression: '"1234".parseInt()',
expectedResult: '''
- (function(x, c) {
- return foo['NumberParsing|parseInt']("1234");
- }(
- 1, null
- ))
- ''');
+ (function(x, c) {
+ return foo['NumberParsing|parseInt']("1234");
+ }(
+ 1,
+ null
+ ))
+ ''');
});
test('private field modification', () async {
@@ -995,7 +1104,8 @@
let _field = dart.privateName(foo, "_field");
return c[_field] = 2;
}(
- 1, null
+ 1,
+ null
))
''');
});
@@ -1008,7 +1118,8 @@
(function(x, c) {
return c.field = 2;
}(
- 1, null
+ 1,
+ null
))
''');
});
@@ -1028,7 +1139,8 @@
(function(x, c) {
return foo.C.staticField = 2;
}(
- 1, null
+ 1,
+ null
))
''');
});
@@ -1041,7 +1153,8 @@
(function(x, c) {
return core.print(x);
}(
- 1, null
+ 1,
+ null
))
''');
});
@@ -1093,7 +1206,10 @@
(function(x, c, y, z) {
return dart.str(x) + "+" + dart.str(y) + "+" + dart.str(z);
}(
- 1, null, 3, 0
+ 1,
+ null,
+ 3,
+ 0
))
''');
});
@@ -1106,7 +1222,10 @@
(function(x, c, y, z) {
return dart.str(y) + "+" + dart.str(z);
}(
- 1, null, 3, 0
+ 1,
+ null,
+ 3,
+ 0
))
''');
});
@@ -1168,7 +1287,7 @@
(function(p) {
return foo.bar(p);
}(
- 1
+ 1
))
''');
});
@@ -1195,7 +1314,7 @@
(function(p) {
return C0 || CT.C0;
}(
- 1
+ 1
))
''');
});
@@ -1211,8 +1330,8 @@
))
''');
},
- skip:
- 'Cannot compile constants optimized away by the frontend'); // https://github.com/dart-lang/sdk/issues/41999
+ skip: 'Cannot compile constants optimized away by the frontend. '
+ 'Issue: https://github.com/dart-lang/sdk/issues/41999');
test('evaluate factory constructor call', () async {
await driver.check(
@@ -1222,7 +1341,7 @@
(function(p) {
return new foo.ValueKey.new("t");
}(
- 1
+ 1
))
''');
});
@@ -1235,9 +1354,476 @@
(function(p) {
return C0 || CT.C0;
}(
- 1
+ 1
))
''');
});
});
+
+ group('Expression compiler tests in constructor:', () {
+ const source = '''
+ extension NumberParsing on String {
+ int parseInt() {
+ return int.parse(this);
+ }
+ }
+
+ int global = 42;
+
+ class C {
+ C(int this.field, int this._field) {
+ int x = 1;
+ /* evaluation placeholder */
+ print(this.field);
+ }
+
+ static int staticField = 0;
+ static int _staticField = 1;
+
+ int _field;
+ int field;
+
+ int methodFieldAccess(int t) {
+ return t + _field + _staticField;
+ }
+
+ Future<int> asyncMethod(int t) async {
+ return t;
+ }
+ }
+
+ main() => 0;
+ ''';
+
+ TestDriver driver;
+
+ setUp(() {
+ driver = TestDriver(options, source);
+ });
+
+ tearDown(() {
+ driver.delete();
+ });
+
+ test('compilation error', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: 'typo',
+ expectedError: "The getter 'typo' isn't defined for the class 'C'");
+ });
+
+ test('local', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: 'x',
+ expectedResult: '''
+ (function(x) {
+ return x;
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+
+ test('this', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: 'this',
+ expectedResult: '''
+ (function(x) {
+ return this;
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+
+ test('expression using locals', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: 'x + 1',
+ expectedResult: '''
+ (function(x) {
+ return dart.notNull(x) + 1;
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+
+ test('expression using static fields', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: 'x + staticField',
+ expectedResult: '''
+ (function(x) {
+ return dart.notNull(x) + dart.notNull(foo.C.staticField);
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+
+ test('expression using private static fields', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: 'x + _staticField',
+ expectedResult: '''
+ (function(x) {
+ let foo = require('foo.dart').foo;
+ let _staticField = dart.privateName(foo, "_staticField");
+ return dart.notNull(x) + dart.notNull(foo.C._staticField);
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+
+ test('expression using fields', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: 'x + field',
+ expectedResult: '''
+ (function(x) {
+ return dart.notNull(x) + dart.notNull(this.field);
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+
+ test('expression using private fields', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: 'x + _field',
+ expectedResult: '''
+ (function(x) {
+ let foo = require('foo.dart').foo;
+ let _field = dart.privateName(foo, "_field");
+ return dart.notNull(x) + dart.notNull(this[_field]);
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+
+ test('expression using globals', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: 'x + global',
+ expectedResult: '''
+ (function(x) {
+ return dart.notNull(x) + dart.notNull(foo.global);
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+
+ test('method call', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: 'methodFieldAccess(2)',
+ expectedResult: '''
+ (function(x) {
+ return this.methodFieldAccess(2);
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+
+ test('async method call', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: 'asyncMethod(2)',
+ expectedResult: '''
+ (function(x) {
+ return this.asyncMethod(2);
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+
+ test('extension method call', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: '"1234".parseInt()',
+ expectedResult: '''
+ (function(x) {
+ return foo['NumberParsing|parseInt']("1234");
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+
+ test('private field modification', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: '_field = 2',
+ expectedResult: '''
+ (function(x) {
+ let foo = require('foo.dart').foo;
+ let _field = dart.privateName(foo, "_field");
+ return this[_field] = 2;
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+
+ test('field modification', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: 'field = 2',
+ expectedResult: '''
+ (function(x) {
+ return this.field = 2;
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+
+ test('private static field modification', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: '_staticField = 2',
+ expectedResult: '''
+ (function(x) {
+ let foo = require('foo.dart').foo;
+ let _staticField = dart.privateName(foo, "_staticField");
+ return foo.C._staticField = 2;
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+
+ test('static field modification', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1'},
+ expression: 'staticField = 2',
+ expectedResult: '''
+ (function(x) {
+ return foo.C.staticField = 2;
+ }.bind(this)(
+ 1
+ ))
+ ''');
+ });
+ });
+
+ group('Expression compiler tests in loops:', () {
+ const source = r'''
+ int globalFunction() {
+ int x = 15;
+ var c = C(1, 2);
+
+ for(int i = 0; i < 10; i++) {
+ /* evaluation placeholder */
+ print('$i+$x');
+ };
+ return 0;
+ }
+
+ main() => 0;
+ ''';
+
+ TestDriver driver;
+ setUp(() {
+ driver = TestDriver(options, source);
+ });
+
+ tearDown(() {
+ driver.delete();
+ });
+
+ test('expression using local', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1', 'c': 'null', 'i': '0'},
+ expression: 'x',
+ expectedResult: '''
+ (function(x, c, i) {
+ return x;
+ }(
+ 1,
+ null,
+ 0
+ ))
+ ''');
+ });
+
+ test('expression using loop variable', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1', 'c': 'null', 'i': '0'},
+ expression: 'i',
+ expectedResult: '''
+ (function(x, c, i) {
+ return i;
+ }(
+ 1,
+ null,
+ 0
+ ))
+ ''');
+ });
+ });
+
+ group('Expression compiler tests in conditional (then):', () {
+ const source = r'''
+ int globalFunction() {
+ int x = 1;
+ var c = C(1, 2);
+
+ if (x == 14) {
+ int y = 3;
+ /* evaluation placeholder */
+ print('$y+$x');
+ } else {
+ int z = 3;
+ print('$z+$x');
+ }
+ return 0;
+ }
+
+ main() => 0;
+ ''';
+
+ TestDriver driver;
+ setUp(() {
+ driver = TestDriver(options, source);
+ });
+
+ tearDown(() {
+ driver.delete();
+ });
+
+ test('expression using local', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1', 'c': 'null', 'y': '3'},
+ expression: 'y',
+ expectedResult: '''
+ (function(x, c, y) {
+ return y;
+ }(
+ 1,
+ null,
+ 3
+ ))
+ ''');
+ });
+
+ test('expression using local out of scope', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1', 'c': 'null', 'y': '3'},
+ expression: 'z',
+ expectedError: "Error: Getter not found: 'z'");
+ });
+ });
+
+ group('Expression compiler tests in conditional (else):', () {
+ const source = r'''
+ int globalFunction() {
+ int x = 1;
+ var c = C(1, 2);
+
+ if (x == 14) {
+ int y = 3;
+ print('$y+$x');
+ } else {
+ int z = 3;
+ /* evaluation placeholder */
+ print('$z+$x');
+ }
+ return 0;
+ }
+
+ main() => 0;
+ ''';
+
+ TestDriver driver;
+ setUp(() {
+ driver = TestDriver(options, source);
+ });
+
+ tearDown(() {
+ driver.delete();
+ });
+
+ test('expression using local', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1', 'c': 'null', 'z': '3'},
+ expression: 'z',
+ expectedResult: '''
+ (function(x, c, z) {
+ return z;
+ }(
+ 1,
+ null,
+ 3
+ ))
+ ''');
+ });
+
+ test('expression using local out of scope', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1', 'c': 'null', 'z': '3'},
+ expression: 'y',
+ expectedError: "Error: Getter not found: 'y'");
+ });
+ });
+
+ group('Expression compiler tests after conditionals:', () {
+ const source = r'''
+ int globalFunction() {
+ int x = 1;
+ var c = C(1, 2);
+
+ if (x == 14) {
+ int y = 3;
+ print('$y+$x');
+ } else {
+ int z = 3;
+ print('$z+$x');
+ }
+ /* evaluation placeholder */
+ return 0;
+ }
+
+ main() => 0;
+ ''';
+
+ TestDriver driver;
+ setUp(() {
+ driver = TestDriver(options, source);
+ });
+
+ tearDown(() {
+ driver.delete();
+ });
+
+ test('expression using local', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1', 'c': 'null'},
+ expression: 'x',
+ expectedResult: '''
+ (function(x, c) {
+ return x;
+ }(
+ 1,
+ null
+ ))
+ ''');
+ });
+
+ test('expression using local out of scope', () async {
+ await driver.check(
+ scope: <String, String>{'x': '1', 'c': 'null'},
+ expression: 'z',
+ expectedError: "Error: Getter not found: 'z'");
+ });
+ });
}
diff --git a/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_test.dart b/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_test.dart
index 65167c7..1735621 100644
--- a/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_test.dart
+++ b/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_test.dart
@@ -115,6 +115,14 @@
B b = new B();
}
+
+extension NumberParsing on String {
+ int parseInt() {
+ var ret = int.parse(this);
+ // line 17
+ return ret;
+ }
+}
''');
var testLibrary = root.resolve('lib/test_library.dart');
@@ -317,6 +325,40 @@
]));
});
+ test(
+ 'can load dependencies and compile expressions in main (extension method)',
+ () async {
+ requestController.add({
+ 'command': 'UpdateDeps',
+ 'inputs': inputs,
+ });
+
+ requestController.add({
+ 'command': 'CompileExpression',
+ 'expression': 'ret',
+ 'line': 17,
+ 'column': 1,
+ 'jsModules': {},
+ 'jsScope': {'ret': 'ret'},
+ 'libraryUri': config.mainModule.libraryUri,
+ 'moduleName': config.mainModule.moduleName,
+ });
+
+ expect(
+ responseController.stream,
+ emitsInOrder([
+ equals({
+ 'succeeded': true,
+ }),
+ equals({
+ 'succeeded': true,
+ 'errors': isEmpty,
+ 'warnings': isEmpty,
+ 'compiledProcedure': contains('return ret;'),
+ })
+ ]));
+ });
+
test('can load dependencies and compile transitive expressions in main',
() async {
requestController.add({
diff --git a/pkg/dev_compiler/test/expression_compiler/scope_offset_test.dart b/pkg/dev_compiler/test/expression_compiler/scope_offset_test.dart
new file mode 100644
index 0000000..0ee38ea
--- /dev/null
+++ b/pkg/dev_compiler/test/expression_compiler/scope_offset_test.dart
@@ -0,0 +1,138 @@
+// 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 'package:dev_compiler/src/kernel/expression_compiler.dart';
+import 'package:front_end/src/api_prototype/standard_file_system.dart';
+import 'package:front_end/src/api_unstable/ddc.dart';
+import 'package:front_end/src/compute_platform_binaries_location.dart';
+import 'package:kernel/ast.dart';
+import 'package:kernel/binary/ast_from_binary.dart';
+import 'package:kernel/src/printer.dart';
+import 'package:test/test.dart';
+
+/// Verbose mode for debugging
+bool get verbose => false;
+
+Uri get sdkRoot => computePlatformBinariesLocation();
+Uri get sdkSummaryPath => sdkRoot.resolve('ddc_sdk.dill');
+Uri get librariesPath => sdkRoot.resolve('lib/libraries.json');
+
+void main(List<String> args) {
+ test('Offsets are present on scoping nodes in SDK', () async {
+ var entity = StandardFileSystem.instance.entityForUri(sdkSummaryPath);
+ var bytes = await entity.readAsBytes();
+
+ var component = Component();
+ BinaryBuilderWithMetadata(bytes, disableLazyReading: true)
+ .readComponent(component, checkCanonicalNames: true, createView: true);
+
+ for (var lib in component.libraries) {
+ ScopeOffsetValidator.validate(lib);
+ }
+ });
+}
+
+class ScopeOffsetValidator extends Visitor<void> {
+ int classCount = 0;
+ int memberCount = 0;
+ int blockCount = 0;
+
+ ScopeOffsetValidator._();
+
+ static void validate(Library library) {
+ var validator = ScopeOffsetValidator._();
+ validator.visitLibrary(library);
+ expect(validator.classCount + validator.memberCount, greaterThan(0),
+ reason: 'Validation was not empty');
+ expect(validator.blockCount, equals(0),
+ reason: 'SDK dill only contains outlines');
+ }
+
+ @override
+ void defaultTreeNode(Node node) {
+ node.visitChildren(this);
+ }
+
+ @override
+ void visitClass(Class cls) {
+ classCount++;
+ expect(
+ cls,
+ const TypeMatcher<Class>()
+ .having((c) => c.fileOffset, '${cls.name} : fileOffset',
+ isNot(equals(-1)))
+ .having((c) => c.fileEndOffset, '${cls.name} : fileEndOffset',
+ isNot(equals(-1))));
+
+ super.visitClass(cls);
+ }
+
+ @override
+ void defaultMember(Member member) {
+ // exclude code that does not correspond to a dart source
+ // location we can set a breakpoint on.
+ var noBreakPointPossible = (member is Constructor)
+ ? member.isSynthetic
+ : (member is Procedure)
+ ? member.isNoSuchMethodForwarder ||
+ member.isAbstract ||
+ member.isForwardingStub
+ : (member is Field)
+ ? member.name.name.contains(redirectingName)
+ : false;
+
+ if (!noBreakPointPossible) {
+ memberCount++;
+ expect(
+ member,
+ const TypeMatcher<Member>()
+ .having(
+ (c) => c.fileOffset,
+ '${member.enclosingClass}.${member.name} : fileOffset',
+ isNot(equals(-1)))
+ .having(
+ (c) => c.fileEndOffset,
+ '${member.enclosingClass}.${member.name} : fileEndOffset',
+ isNot(equals(-1))));
+
+ super.defaultMember(member);
+ }
+ }
+
+ @override
+ void visitFunctionNode(FunctionNode fun) {
+ expect(
+ fun,
+ const TypeMatcher<FunctionNode>()
+ .having(
+ (c) => c.fileOffset,
+ '${fun.parent.toText(astTextStrategyForTesting)} : fileOffset',
+ isNot(equals(-1)))
+ .having(
+ (c) => c.fileEndOffset,
+ '${fun.parent.toText(astTextStrategyForTesting)} : fileEndOffset',
+ isNot(equals(-1))));
+
+ super.visitFunctionNode(fun);
+ }
+
+ @override
+ void visitBlock(Block block) {
+ blockCount++;
+ expect(
+ block,
+ const TypeMatcher<FunctionNode>().having(
+ (c) => c.fileOffset,
+ '${block.toText(astTextStrategyForTesting)} : fileOffset',
+ isNot(equals(-1))));
+
+ var fileEndOffset = FileEndOffsetCalculator.calculateEndOffset(block);
+ expect(fileEndOffset, isNot(equals(-1)),
+ reason: '${block.toText(astTextStrategyForTesting)} : fileOffset');
+
+ super.visitBlock(block);
+ }
+}
diff --git a/pkg/front_end/lib/src/api_unstable/ddc.dart b/pkg/front_end/lib/src/api_unstable/ddc.dart
index 3c0befd..18a00ef 100644
--- a/pkg/front_end/lib/src/api_unstable/ddc.dart
+++ b/pkg/front_end/lib/src/api_unstable/ddc.dart
@@ -67,7 +67,7 @@
export '../fasta/incremental_compiler.dart' show IncrementalCompiler;
export '../fasta/kernel/redirecting_factory_body.dart'
- show RedirectingFactoryBody, isRedirectingFactoryField;
+ show RedirectingFactoryBody, isRedirectingFactoryField, redirectingName;
export '../fasta/type_inference/type_schema_environment.dart'
show TypeSchemaEnvironment;
diff --git a/pkg/front_end/lib/src/fasta/builder/field_builder.dart b/pkg/front_end/lib/src/fasta/builder/field_builder.dart
index b9b46e8..5584775 100644
--- a/pkg/front_end/lib/src/fasta/builder/field_builder.dart
+++ b/pkg/front_end/lib/src/fasta/builder/field_builder.dart
@@ -737,6 +737,7 @@
abstract class AbstractLateFieldEncoding implements FieldEncoding {
final String name;
final int fileOffset;
+ final int fileEndOffset;
DartType _type;
Field _field;
Field _lateIsSetField;
@@ -772,6 +773,7 @@
bool isCovariant,
late_lowering.IsSetStrategy isSetStrategy)
: fileOffset = charOffset,
+ fileEndOffset = charEndOffset,
_isSetStrategy = isSetStrategy,
_forceIncludeIsSetField =
isSetStrategy == late_lowering.IsSetStrategy.forceUseIsSetField {
@@ -796,9 +798,15 @@
break;
}
_lateGetter = new Procedure(
- null, ProcedureKind.Getter, new FunctionNode(null),
- fileUri: fileUri, reference: getterReferenceFrom?.reference)
+ null,
+ ProcedureKind.Getter,
+ new FunctionNode(null)
+ ..fileOffset = charOffset
+ ..fileEndOffset = charEndOffset,
+ fileUri: fileUri,
+ reference: getterReferenceFrom?.reference)
..fileOffset = charOffset
+ ..fileEndOffset = charEndOffset
..isNonNullableByDefault = true;
_lateSetter = _createSetter(name, fileUri, charOffset, setterReferenceFrom,
isCovariant: isCovariant);
@@ -913,10 +921,13 @@
null,
ProcedureKind.Setter,
new FunctionNode(null,
- positionalParameters: [parameter], returnType: const VoidType()),
+ positionalParameters: [parameter], returnType: const VoidType())
+ ..fileOffset = charOffset
+ ..fileEndOffset = fileEndOffset,
fileUri: fileUri,
reference: referenceFrom?.reference)
..fileOffset = charOffset
+ ..fileEndOffset = fileEndOffset
..isNonNullableByDefault = true;
}
@@ -1496,7 +1507,9 @@
null,
ProcedureKind.Setter,
new FunctionNode(null,
- positionalParameters: [parameter], returnType: const VoidType()),
+ positionalParameters: [parameter], returnType: const VoidType())
+ ..fileOffset = charOffset
+ ..fileEndOffset = charEndOffset,
fileUri: fileUri,
reference: setterReference?.reference)
..fileOffset = charOffset
diff --git a/pkg/front_end/lib/src/fasta/builder/procedure_builder.dart b/pkg/front_end/lib/src/fasta/builder/procedure_builder.dart
index 6ae4f6d..654aef2 100644
--- a/pkg/front_end/lib/src/fasta/builder/procedure_builder.dart
+++ b/pkg/front_end/lib/src/fasta/builder/procedure_builder.dart
@@ -527,24 +527,29 @@
..fileOffset = fileOffset)
..fileOffset = fileOffset;
- FunctionExpression closure = new FunctionExpression(new FunctionNode(
- closureBody,
- typeParameters: closureTypeParameters,
- positionalParameters: closurePositionalParameters,
- namedParameters: closureNamedParameters,
- requiredParameterCount: _procedure.function.requiredParameterCount - 1,
- returnType: closureReturnType))
+ FunctionExpression closure = new FunctionExpression(
+ new FunctionNode(closureBody,
+ typeParameters: closureTypeParameters,
+ positionalParameters: closurePositionalParameters,
+ namedParameters: closureNamedParameters,
+ requiredParameterCount:
+ _procedure.function.requiredParameterCount - 1,
+ returnType: closureReturnType)
+ ..fileOffset = fileOffset
+ ..fileEndOffset = fileEndOffset)
..fileOffset = fileOffset;
_extensionTearOff
..name = new Name(
'${extensionBuilder.name}|get#${name}', libraryBuilder.library)
- ..function = new FunctionNode(
+ ..function = (new FunctionNode(
new ReturnStatement(closure)..fileOffset = fileOffset,
typeParameters: tearOffTypeParameters,
positionalParameters: [extensionThis],
requiredParameterCount: 1,
returnType: closure.function.computeFunctionType(library.nonNullable))
+ ..fileOffset = fileOffset
+ ..fileEndOffset = fileEndOffset)
..fileUri = fileUri
..fileOffset = fileOffset
..fileEndOffset = fileEndOffset;
diff --git a/pkg/front_end/lib/src/fasta/incremental_compiler.dart b/pkg/front_end/lib/src/fasta/incremental_compiler.dart
index 5044b26..cb94983 100644
--- a/pkg/front_end/lib/src/fasta/incremental_compiler.dart
+++ b/pkg/front_end/lib/src/fasta/incremental_compiler.dart
@@ -7,6 +7,8 @@
import 'dart:async' show Future;
import 'package:front_end/src/api_prototype/experimental_flags.dart';
+import 'package:front_end/src/api_prototype/front_end.dart';
+import 'package:front_end/src/fasta/fasta_codes.dart';
import 'package:kernel/binary/ast_from_binary.dart'
show
BinaryBuilderWithMetadata,
@@ -1668,10 +1670,27 @@
userCode.loader.seenMessages.clear();
for (TypeParameter typeParam in typeDefinitions) {
- if (!isLegalIdentifier(typeParam.name)) return null;
+ if (!isLegalIdentifier(typeParam.name)) {
+ userCode.loader.addProblem(
+ templateIncrementalCompilerIllegalTypeParameter
+ .withArguments('$typeParam'),
+ typeParam.fileOffset,
+ 0,
+ libraryUri);
+ return null;
+ }
}
for (String name in definitions.keys) {
- if (!isLegalIdentifier(name)) return null;
+ if (!isLegalIdentifier(name)) {
+ userCode.loader.addProblem(
+ templateIncrementalCompilerIllegalParameter.withArguments(name),
+ // TODO: pass variable declarations instead of
+ // parameter names for proper location detection.
+ -1,
+ -1,
+ libraryUri);
+ return null;
+ }
}
SourceLibraryBuilder debugLibrary = new SourceLibraryBuilder(
diff --git a/pkg/front_end/messages.status b/pkg/front_end/messages.status
index 1cb6060..969f40c 100644
--- a/pkg/front_end/messages.status
+++ b/pkg/front_end/messages.status
@@ -394,6 +394,10 @@
IncorrectTypeArgumentQualifiedInferredWarning/example: Fail
IncorrectTypeArgumentQualifiedWarning/example: Fail
IncorrectTypeArgumentWarning/example: Fail
+IncrementalCompilerIllegalParameter/analyzerCode: Fail
+IncrementalCompilerIllegalParameter/example: Fail
+IncrementalCompilerIllegalTypeParameter/analyzerCode: Fail
+IncrementalCompilerIllegalTypeParameter/example: Fail
InitializerForStaticField/example: Fail
InitializerOutsideConstructor/example: Fail
InputFileNotFound/analyzerCode: Fail
diff --git a/pkg/front_end/messages.yaml b/pkg/front_end/messages.yaml
index 905e30c..14e312f 100644
--- a/pkg/front_end/messages.yaml
+++ b/pkg/front_end/messages.yaml
@@ -1892,6 +1892,12 @@
template: "Unsupported nullability value '#string' on type '#type'."
severity: INTERNAL_PROBLEM
+IncrementalCompilerIllegalParameter:
+ template: "Illegal parameter name '#string' found during expression compilation."
+
+IncrementalCompilerIllegalTypeParameter:
+ template: "Illegal type parameter name '#string' found during expression compilation."
+
LocalDefinitionHidesExport:
template: "Local definition of '#name' hides export from '#uri'."
severity: IGNORED
diff --git a/pkg/front_end/testcases/expression/invalid_type_variable.expression.yaml.expect b/pkg/front_end/testcases/expression/invalid_type_variable.expression.yaml.expect
index ba1f145..792e96a 100644
--- a/pkg/front_end/testcases/expression/invalid_type_variable.expression.yaml.expect
+++ b/pkg/front_end/testcases/expression/invalid_type_variable.expression.yaml.expect
@@ -1,3 +1,4 @@
Errors: {
+ pkg/front_end/testcases/expression/main.dart: Error: Illegal type parameter name 'TypeParameter(a#b)' found during expression compilation.
}
<no procedure>
diff --git a/pkg/front_end/testcases/expression/invalid_variable.expression.yaml.expect b/pkg/front_end/testcases/expression/invalid_variable.expression.yaml.expect
index ba1f145..a093fa5 100644
--- a/pkg/front_end/testcases/expression/invalid_variable.expression.yaml.expect
+++ b/pkg/front_end/testcases/expression/invalid_variable.expression.yaml.expect
@@ -1,3 +1,4 @@
Errors: {
+ pkg/front_end/testcases/expression/main.dart: Error: Illegal parameter name 'a#b' found during expression compilation.
}
<no procedure>
diff --git a/pkg/kernel/binary.md b/pkg/kernel/binary.md
index aade49b..de1281f 100644
--- a/pkg/kernel/binary.md
+++ b/pkg/kernel/binary.md
@@ -143,7 +143,7 @@
type ComponentFile {
UInt32 magic = 0x90ABCDEF;
- UInt32 formatVersion = 46;
+ UInt32 formatVersion = 47;
Byte[10] shortSdkHash;
List<String> problemsAsJson; // Described in problems.md.
Library[] libraries;
@@ -1034,6 +1034,7 @@
type Block extends Statement {
Byte tag = 62;
+ FileOffset fileOffset;
List<Statement> statements;
}
diff --git a/pkg/kernel/lib/binary/ast_from_binary.dart b/pkg/kernel/lib/binary/ast_from_binary.dart
index 01e245c..75e552b 100644
--- a/pkg/kernel/lib/binary/ast_from_binary.dart
+++ b/pkg/kernel/lib/binary/ast_from_binary.dart
@@ -2124,9 +2124,10 @@
Block readBlock() {
int stackHeight = variableStack.length;
+ var offset = readOffset();
var body = readStatementList();
variableStack.length = stackHeight;
- return new Block(body);
+ return new Block(body)..fileOffset = offset;
}
AssertBlock readAssertBlock() {
diff --git a/pkg/kernel/lib/binary/ast_to_binary.dart b/pkg/kernel/lib/binary/ast_to_binary.dart
index 2dbc82f..f6543c0 100644
--- a/pkg/kernel/lib/binary/ast_to_binary.dart
+++ b/pkg/kernel/lib/binary/ast_to_binary.dart
@@ -1807,6 +1807,7 @@
_variableIndexer ??= new VariableIndexer();
_variableIndexer.pushScope();
writeByte(Tag.Block);
+ writeOffset(node.fileOffset);
writeNodeList(node.statements);
_variableIndexer.popScope();
}
diff --git a/pkg/kernel/lib/binary/tag.dart b/pkg/kernel/lib/binary/tag.dart
index 2bbc968..4fdf50b 100644
--- a/pkg/kernel/lib/binary/tag.dart
+++ b/pkg/kernel/lib/binary/tag.dart
@@ -146,7 +146,7 @@
/// Internal version of kernel binary format.
/// Bump it when making incompatible changes in kernel binaries.
/// Keep in sync with runtime/vm/kernel_binary.h, pkg/kernel/binary.md.
- static const int BinaryFormatVersion = 46;
+ static const int BinaryFormatVersion = 47;
}
abstract class ConstantTag {
diff --git a/pkg/kernel/lib/clone.dart b/pkg/kernel/lib/clone.dart
index 507d6a1..602ec1c7 100644
--- a/pkg/kernel/lib/clone.dart
+++ b/pkg/kernel/lib/clone.dart
@@ -330,7 +330,8 @@
}
visitBlock(Block node) {
- return new Block(node.statements.map(clone).toList());
+ return new Block(node.statements.map(clone).toList())
+ ..fileOffset = _cloneFileOffset(node.fileOffset);
}
visitAssertBlock(AssertBlock node) {
diff --git a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
index 4df9ad9..10d5611 100644
--- a/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
+++ b/runtime/vm/compiler/frontend/kernel_binary_flowgraph.cc
@@ -4014,6 +4014,7 @@
Fragment instructions;
instructions += EnterScope(offset);
+ ReadPosition();
intptr_t list_length = ReadListLength(); // read number of statements.
for (intptr_t i = 0; i < list_length; ++i) {
if (instructions.is_open()) {
diff --git a/runtime/vm/compiler/frontend/kernel_fingerprints.cc b/runtime/vm/compiler/frontend/kernel_fingerprints.cc
index 51ddf29..ed382e7 100644
--- a/runtime/vm/compiler/frontend/kernel_fingerprints.cc
+++ b/runtime/vm/compiler/frontend/kernel_fingerprints.cc
@@ -583,6 +583,7 @@
CalculateExpressionFingerprint(); // read expression.
return;
case kBlock:
+ ReadPosition();
CalculateStatementListFingerprint();
return;
case kEmptyStatement:
diff --git a/runtime/vm/compiler/frontend/kernel_translation_helper.cc b/runtime/vm/compiler/frontend/kernel_translation_helper.cc
index ff40a8b..fa4f92a 100644
--- a/runtime/vm/compiler/frontend/kernel_translation_helper.cc
+++ b/runtime/vm/compiler/frontend/kernel_translation_helper.cc
@@ -2510,6 +2510,7 @@
SkipExpression(); // read expression.
return;
case kBlock:
+ ReadPosition();
SkipStatementList();
return;
case kEmptyStatement:
diff --git a/runtime/vm/compiler/frontend/scope_builder.cc b/runtime/vm/compiler/frontend/scope_builder.cc
index 56e1dda..6e65b8d 100644
--- a/runtime/vm/compiler/frontend/scope_builder.cc
+++ b/runtime/vm/compiler/frontend/scope_builder.cc
@@ -993,7 +993,7 @@
intptr_t offset = helper_.ReaderOffset() - 1; // -1 to include tag byte.
EnterScope(offset);
-
+ helper_.ReadPosition(); // read block start offset.
intptr_t list_length =
helper_.ReadListLength(); // read number of statements.
for (intptr_t i = 0; i < list_length; ++i) {
diff --git a/runtime/vm/kernel_binary.h b/runtime/vm/kernel_binary.h
index 1b53aaa..49898fe 100644
--- a/runtime/vm/kernel_binary.h
+++ b/runtime/vm/kernel_binary.h
@@ -20,8 +20,8 @@
static const uint32_t kMagicProgramFile = 0x90ABCDEFu;
// Both version numbers are inclusive.
-static const uint32_t kMinSupportedKernelFormatVersion = 46;
-static const uint32_t kMaxSupportedKernelFormatVersion = 46;
+static const uint32_t kMinSupportedKernelFormatVersion = 47;
+static const uint32_t kMaxSupportedKernelFormatVersion = 47;
// Keep in sync with package:kernel/lib/binary/tag.dart
#define KERNEL_TAG_LIST(V) \