Reland "[ddc] Add module local caches for new types"

This is a reland of commit a9fc9ffc4d

Additional changes:

- Set incremental mode on generic class table
  - fixes expression evaluation failure discovered by dwds tests
    (generic class table was not always defined in compiled expression)
- Allow expression evaluation while the app is running in e2e suite
- Add regression tests for the expression evaluation failure.

Original change's description:
> [ddc] Add module local caches for new types
>
> - Provides fast access for types that are used multiple times in the
>   same module.
> - Enable the existing type table cache when running with new types.
> - Add a similar cache for instantiated generic classes. This cache
>   is used in the current type system as well to help keep the
>   difference between types and classes more clear.
>
> Issue: https://github.com/dart-lang/sdk/issues/48585
> Change-Id: I32103cf0c0bcf9b9771e789c0d04e63a4365a066
> Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/321320
> Commit-Queue: Nicholas Shahan <nshahan@google.com>
> Reviewed-by: Mark Zhou <markzipan@google.com>

Change-Id: I9c31d1d07d7f9bb15645ac9aa6e91d35e8906e85
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/323501
Reviewed-by: Mark Zhou <markzipan@google.com>
Commit-Queue: Anna Gringauze <annagrin@google.com>
Reviewed-by: Nicholas Shahan <nshahan@google.com>
This commit is contained in:
Anna Gringauze 2023-09-01 21:10:09 +00:00 committed by Commit Queue
parent 36c6daa920
commit 3740e620cd
11 changed files with 616 additions and 306 deletions

View file

@ -172,6 +172,11 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
/// Table of named and possibly hoisted types.
late TypeTable _typeTable;
/// Table of instantiated generic class references.
///
/// Provides a cache for the instantiated generic types local to a module.
late TypeTable _genericClassTable;
/// The global extension type table.
// TODO(jmesserly): rename to `_nativeTypes`
final NativeTypeSet _extensionTypes;
@ -367,6 +372,7 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
_assertInteropMethod = sdk.getTopLevelMember(
'dart:_runtime', 'assertInterop') as Procedure,
_futureOrNormalizer = FutureOrNormalizer(_coreTypes),
_extensionTypeEraser = ExtensionTypeEraser(),
_typeRecipeGenerator = TypeRecipeGenerator(_coreTypes, _hierarchy),
_extensionIndex =
ExtensionIndex(_coreTypes, _staticTypeContext.typeEnvironment);
@ -390,6 +396,8 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
final FutureOrNormalizer _futureOrNormalizer;
final ExtensionTypeEraser _extensionTypeEraser;
/// Module can be emitted only once, and the compiler can be reused after
/// only in incremental mode, for expression compilation only.
js_ast.Program emitModule(Component component) {
@ -451,7 +459,8 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
}
_nullableInference.allowNotNullDeclarations = isBuildingSdk;
_typeTable = TypeTable(runtimeCall);
_typeTable = TypeTable('T', runtimeCall);
_genericClassTable = TypeTable('G', runtimeCall);
// Collect all class/type Element -> Node mappings
// in case we need to forward declare any classes.
@ -616,6 +625,10 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
items.addAll(_typeTable.dischargeBoundTypes());
_ticker?.logMs('Emitted type table');
// Emit the hoisted instantiated generic class table cache variables
items.addAll(_genericClassTable.dischargeBoundTypes());
_ticker?.logMs('Emitted instantiated generic class table');
var module = finishModule(items, _options.moduleName,
header: generateCompilationHeader());
_ticker?.logMs('Finished emitting module');
@ -1057,8 +1070,13 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
body = js_ast.Statement.from([body, varianceStatement]);
}
var typeConstructor = js.call('(#) => { #; #; return #; }',
[jsFormals, _typeTable.dischargeFreeTypes(formals), body, className]);
var typeConstructor = js.call('(#) => { #; #; #; return #; }', [
jsFormals,
_typeTable.dischargeFreeTypes(formals),
_genericClassTable.dischargeFreeTypes(formals),
body,
className
]);
var genericArgs = [
typeConstructor,
@ -1268,6 +1286,7 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
if (t is RecordType) {
return t.positional.any(defer) || t.named.any((n) => defer(n.type));
}
if (t is ExtensionType) return defer(t.typeErasure);
return false;
}
@ -1631,7 +1650,10 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
_classEmittingSignatures = c;
var interfaces = c.implementedTypes.toList()..addAll(c.onClause);
if (interfaces.isNotEmpty) {
if (interfaces.isNotEmpty &&
// New runtime types don't use this data structure to lookup interfaces
// a class implements.
!_options.newRuntimeTypes) {
body.add(js.statement('#[#] = () => [#];', [
className,
runtimeCall('implements'),
@ -3260,6 +3282,7 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
if (type is TypedefType) {
return type.typeArguments.every(_canEmitTypeAtTopLevel);
}
if (type is ExtensionType) return _canEmitTypeAtTopLevel(type.typeErasure);
return true;
}
@ -3304,6 +3327,8 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
// An environment with a single type parameter can be simplified to
// just that parameter.
env = _emitTypeParameter(environment.parameters.single);
// Skip a no-op evaluation and just return the parameter.
if (recipe == '0') return env;
} else {
var environmentTypes = environment.parameters;
// Create a dummy interface type to "hold" type arguments.
@ -3334,16 +3359,15 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
}
}
// TODO(nshahan) Avoid calling _emitType when we actually want a
// reference to an rti that already exists in scope.
if (type is TypeParameterType && type.isPotentiallyNonNullable) {
return _emitTypeParameterType(type, emitNullability: false);
}
var normalizedType = _futureOrNormalizer.normalize(type);
var normalizedType =
_futureOrNormalizer.normalize(_extensionTypeEraser.erase(type));
try {
var result = _typeRecipeGenerator.recipeInEnvironment(
normalizedType, _currentTypeEnvironment);
return evalInEnvironment(result.requiredEnvironment, result.recipe);
var typeRep =
evalInEnvironment(result.requiredEnvironment, result.recipe);
if (_cacheTypes) typeRep = _typeTable.nameType(normalizedType, typeRep);
return typeRep;
} on UnsupportedError catch (e) {
_typeCompilationError(normalizedType, e.message ?? 'Unknown Error');
}
@ -3496,7 +3520,10 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
if (args.any((a) => a != const DynamicType())) {
jsArgs = args.map(_emitType);
}
if (jsArgs != null) return _emitGenericClassType(type, jsArgs);
if (jsArgs != null) {
return _genericClassTable.nameType(
type, _emitGenericClassType(type, jsArgs));
}
return _emitTopLevelNameNoExternalInterop(type.classNode);
}
@ -3567,11 +3594,12 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
bool get _emittingClassExtends =>
_currentClass != null && identical(_currentClass, _classEmittingExtends);
bool get _cacheTypes =>
!_emittingDeferredType &&
!_emittingClassExtends &&
!_emittingClassSignatures ||
_currentFunction != null;
bool get _cacheTypes => _options.newRuntimeTypes
? !_emittingDeferredType && !_emittingClassExtends
: !_emittingDeferredType &&
!_emittingClassExtends &&
!_emittingClassSignatures ||
_currentFunction != null;
js_ast.Expression _emitGenericClassType(
InterfaceType t, Iterable<js_ast.Expression> typeArgs) {
@ -3812,6 +3840,7 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
_uriContainer = ModuleItemContainer<String>.asArray('I');
_typeTable.typeContainer.setIncrementalMode();
_genericClassTable.typeContainer.setIncrementalMode();
}
/// Emits function after initial compilation.
@ -3853,6 +3882,7 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
var body = js_ast.Block([
...extensionSymbols,
..._typeTable.dischargeBoundTypes(),
..._genericClassTable.dischargeBoundTypes(),
...symbolContainer.emit(),
..._emitConstTable(),
..._uriContainer.emit(),
@ -6851,23 +6881,21 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
js_ast.Expression _emitMapImplType(InterfaceType type, {bool? identity}) {
var typeArgs = type.typeArguments;
if (typeArgs.isEmpty) {
return _emitInterfaceType(type, emitNullability: false);
return _emitClassRef(type);
}
identity ??= _typeRep.isPrimitive(typeArgs[0]);
var c = identity ? _identityHashMapImplClass : _linkedHashMapImplClass;
return _emitInterfaceType(InterfaceType(c, Nullability.legacy, typeArgs),
emitNullability: false);
return _emitClassRef(InterfaceType(c, Nullability.legacy, typeArgs));
}
js_ast.Expression _emitSetImplType(InterfaceType type, {bool? identity}) {
var typeArgs = type.typeArguments;
if (typeArgs.isEmpty) {
return _emitInterfaceType(type, emitNullability: false);
return _emitClassRef(type);
}
identity ??= _typeRep.isPrimitive(typeArgs[0]);
var c = identity ? _identityHashSetImplClass : _linkedHashSetImplClass;
return _emitInterfaceType(InterfaceType(c, Nullability.legacy, typeArgs),
emitNullability: false);
return _emitClassRef(InterfaceType(c, Nullability.legacy, typeArgs));
}
js_ast.Expression _emitObjectLiteral(Arguments node, Member ctor) {
@ -7199,9 +7227,8 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
if (itemType == const DynamicType()) return list;
// Call `new JSArray<E>.of(list)`
var arrayType = _emitInterfaceType(
InterfaceType(_jsArrayClass, Nullability.legacy, [itemType]),
emitNullability: false);
var arrayType = _emitClassRef(
InterfaceType(_jsArrayClass, Nullability.nonNullable, [itemType]));
return js.call('#.of(#)', [arrayType, list]);
}
@ -7217,10 +7244,8 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
js_ast.Expression visitSetLiteral(SetLiteral node) {
// TODO(markzipan): remove const check when we use front-end const eval
if (!node.isConst) {
var setType = _emitInterfaceType(
InterfaceType(
_linkedHashSetClass, Nullability.legacy, [node.typeArgument]),
emitNullability: false);
var setType = _emitClassRef(InterfaceType(
_linkedHashSetClass, Nullability.legacy, [node.typeArgument]));
if (node.expressions.isEmpty) {
return js.call('#.new()', [setType]);
}
@ -7674,8 +7699,7 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
var type = node
.getType(_staticTypeContext)
.withDeclaredNullability(Nullability.nonNullable);
var classRef =
_emitInterfaceType(type as InterfaceType, emitNullability: false);
var classRef = _emitClassRef(type as InterfaceType);
var prototype = js.call('#.prototype', [classRef]);
var properties = [
if (_options.newRuntimeTypes && type.typeArguments.isNotEmpty)

View file

@ -379,13 +379,9 @@ class ExpressionCompiler {
var args = localJsScope.join(',\n ');
jsExpression = jsExpression.split('\n').join('\n ');
var callExpression = '\ntry {'
'\n ($jsExpression('
var callExpression = '\n ($jsExpression('
'\n $args'
'\n ))'
'\n} catch (error) {'
'\n error.name + ": " + error.message;'
'\n}';
'\n ))';
_log('Compiled expression \n$expression to $callExpression');
return callExpression;

View file

@ -60,6 +60,7 @@ class JSTypeRep extends SharedJSTypeRep<DartType> {
if (type == const DynamicType() || type == const VoidType()) {
return JSType.jsUnknown;
}
if (type is ExtensionType) typeFor(type.typeErasure);
return JSType.jsObject;
}

View file

@ -6,6 +6,7 @@ import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/kernel.dart' hide Pattern;
import 'package:kernel/src/replacement_visitor.dart';
Constructor? unnamedConstructor(Class c) =>
c.constructors.firstWhereOrNull((c) => c.name.text == '');
@ -290,6 +291,7 @@ class LabelContinueFinder extends RecursiveVisitor<void> {
/// code if used in an assert.
bool isKnownDartTypeImplementor(DartType t) {
return t is DynamicType ||
t is ExtensionType ||
t is FunctionType ||
t is FutureOrType ||
t is InterfaceType ||
@ -366,3 +368,15 @@ class InterfaceTypeExtractor extends RecursiveVisitor<DartType> {
return _found;
}
}
class ExtensionTypeEraser extends ReplacementVisitor {
const ExtensionTypeEraser();
/// Erases all `ExtensionType` nodes found in [type].
DartType erase(DartType type) =>
type.accept1(this, Variance.unrelated) ?? type;
@override
DartType? visitExtensionType(ExtensionType node, int variance) =>
node.typeErasure.accept1(this, Variance.unrelated) ?? node.typeErasure;
}

View file

@ -35,6 +35,8 @@ Set<TypeParameter> freeTypeParameters(DartType t) {
} else if (t is RecordType) {
t.positional.forEach((p) => find(p));
t.named.forEach((n) => find(n.type));
} else if (t is ExtensionType) {
find(t.typeErasure);
}
}
@ -102,6 +104,7 @@ String _typeString(DartType type, {bool flat = false}) {
}
return 'Rec${nullability}Of$elements';
}
if (type is ExtensionType) return _typeString(type.typeErasure);
return 'invalid';
}
@ -120,12 +123,13 @@ class TypeTable {
final _unboundTypeIds = HashMap<DartType, js_ast.Identifier>();
/// Holds JS type generators keyed by their underlying DartType.
final typeContainer = ModuleItemContainer<DartType>.asObject('T',
keyToString: (DartType t) => escapeIdentifier(_typeString(t))!);
final ModuleItemContainer<DartType> typeContainer;
final js_ast.Expression Function(String, [List<Object>]) _runtimeCall;
TypeTable(this._runtimeCall);
TypeTable(String name, this._runtimeCall)
: typeContainer = ModuleItemContainer<DartType>.asObject(name,
keyToString: (DartType t) => escapeIdentifier(_typeString(t))!);
/// Returns true if [type] is already recorded in the table.
bool _isNamed(DartType type) =>

View file

@ -8,8 +8,11 @@ import '../shared_test_options.dart';
import 'expression_compiler_e2e_suite.dart';
void main(List<String> args) async {
final debug = false;
var driver = await ExpressionEvaluationTestDriver.init();
var setup = SetupCompilerOptions(args: args);
setup.options.verbose = debug;
group('Asserts', () {
const source = r'''
@ -34,27 +37,26 @@ void main(List<String> args) async {
if (setup.enableAsserts) {
group('enabled |', () {
test('dart.web.assertions_enabled is set', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp', expression: 'b', expectedResult: 'true');
});
test('assert errors in the source code', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'myAssert()',
expectedResult: allOf(
expectedError: allOf(
contains('Error: Assertion failed:'),
contains('test.dart:8:16'),
contains('false'),
contains('is not true'),
));
});
test('assert errors in evaluated expression', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: '() { assert(false); return 0; } ()',
expectedResult: allOf(
expectedError: allOf(
contains('Error: Assertion failed:'),
contains('<unknown source>:-1:-1'),
contains('BoolLiteral(false)'),
@ -67,19 +69,19 @@ void main(List<String> args) async {
if (!setup.enableAsserts) {
group('disabled |', () {
test('dart.web.assertions_enabled is not set', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp', expression: 'b', expectedResult: 'false');
});
test('no assert errors in the source code', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'myAssert()',
expectedResult: '0');
});
test('no assert errors in evaluated expression', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: '() { assert(false); return 0; } ()',
expectedResult: '0');

View file

@ -110,31 +110,31 @@ void runSharedTests(
});
test('in top level method', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'topLevelMethod(param3: 3, 1, param4: "four", "two")',
expectedResult: '1, two, 3, four');
});
test('in local method', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'topLevelMethod(param3: 3, 1, param4: "four", "two")',
expectedResult: '1, two, 3, four');
});
test('in class constructor', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'C(param3: 3, 1, param4: "four", "two").toString()',
expectedResult: '1, two, 3, four');
});
test('in class static method', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'C.staticMethod(param3: 3, 1, param4: "four", "two")',
expectedResult: '1, two, 3, four');
});
test('in class instance method', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'c.instanceMethod(param3: 3, 1, param4: "four", "two")',
expectedResult: '1, two, 3, four');
@ -179,27 +179,27 @@ void runSharedTests(
});
test('in constructor mixed with regular parameters', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp', expression: 'c.i1', expectedResult: '1');
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp', expression: 'c.i', expectedResult: '2');
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp', expression: 'c.i2', expectedResult: '3');
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp', expression: 'c.s', expectedResult: 'bar');
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp', expression: 'c.d', expectedResult: '3.14');
});
test('in named constructor mixed with regular parameters', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp', expression: 'c2.i1', expectedResult: '10');
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp', expression: 'c2.i', expectedResult: '20');
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp', expression: 'c2.i2', expectedResult: '30');
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp', expression: 'c2.s', expectedResult: 'default');
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp', expression: 'c2.d', expectedResult: '2.71');
});
});
@ -245,61 +245,61 @@ void runSharedTests(
});
test('evaluate to the correct string', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'E.id_string.toString()',
expectedResult: 'E.id_string');
});
test('evaluate to the correct index', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'E.id_string.index',
expectedResult: '2');
});
test('compare properly against themselves', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'e == E.id_string && E.id_string == E.id_string',
expectedResult: 'true');
});
test('compare properly against other enums', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'e != E2.id_string && E.id_string != E2.id_string',
expectedResult: 'true');
});
test('with instance methods', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'E.id_bool.instanceMethod()',
expectedResult: '42');
});
test('with instance methods from local instance', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'e.instanceMethod()',
expectedResult: '13');
});
test('with getters', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'E.id_int.fieldGetter',
expectedResult: '0');
});
test('with getters from local instance', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'e.fieldGetter',
expectedResult: 'hello world');
});
test('with mixin calls', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'E.id_string.mixinMethod()',
expectedResult: '200');
});
test('with mixin calls through overridden indices', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'E2.v2.mixinMethod()',
expectedResult: '100');

View file

@ -87,84 +87,84 @@ void runSharedTests(
});
test('simple record', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'r.toString()',
expectedResult: '(true, 3)');
});
test('simple record type', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'r.runtimeType.toString()',
expectedResult: '(bool, int)');
});
test('simple record field one', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'r.\$1.toString()',
expectedResult: 'true');
});
test('simple record field two', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'r.\$2.toString()',
expectedResult: '3');
});
test('complex record', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'cr.toString()',
expectedResult: '(true, {a: 1, b: 2})');
});
test('complex record type', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'cr.runtimeType.toString()',
expectedResult: '(bool, IdentityMap<String, int>)');
});
test('complex record field one', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'cr.\$1.toString()',
expectedResult: 'true');
});
test('complex record field two', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'cr.\$2.toString()',
expectedResult: '{a: 1, b: 2}');
});
test('nested record', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'nr.toString()',
expectedResult: '(true, (false, 3))');
});
test('nested record type', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'nr.runtimeType.toString()',
expectedResult: '(bool, (bool, int))');
});
test('nested record field one', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'nr.\$1.toString()',
expectedResult: 'true');
});
test('nested record field two', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp',
expression: 'nr.\$2.toString()',
expectedResult: '(false, 3)');
@ -206,40 +206,40 @@ void runSharedTests(
});
test('first case match', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp1', expression: 'a.toString()', expectedResult: '1');
});
test('second case match', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp2',
expression: 'a.toString()',
expectedResult: '10');
});
test('default case match', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp3',
expression: 'obj.toString()',
expectedResult: '0');
});
test('first case match result', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp4',
expression: 'one.toString()',
expectedResult: '1');
});
test('second case match result', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp4',
expression: 'ten.toString()',
expectedResult: '10');
});
test('default match result', () async {
await driver.check(
await driver.checkInFrame(
breakpointId: 'bp4',
expression: 'zero.toString()',
expectedResult: '0');

View file

@ -23,19 +23,22 @@ class ExpressionEvaluationTestDriver {
final Directory chromeDir;
final wip.WipConnection connection;
final wip.WipDebugger debugger;
final wip.WipRuntime runtime;
final ExecutionContext executionContext;
late TestExpressionCompiler compiler;
late Uri htmlBootstrapper;
late Uri input;
late Uri output;
late Uri packagesFile;
late String preemptiveBp;
String? preemptiveBp;
late SetupCompilerOptions setup;
late String source;
late Directory testDir;
late String dartSdkPath;
ExpressionEvaluationTestDriver._(
this.chrome, this.chromeDir, this.connection, this.debugger);
this.chrome, this.chromeDir, this.connection, this.debugger, this.runtime)
: executionContext = ExecutionContext(runtime);
/// Initializes a Chrome browser instance, tab connection, and debugger.
static Future<ExpressionEvaluationTestDriver> init() async {
@ -79,19 +82,27 @@ class ExpressionEvaluationTestDriver {
await connection.page.enable().timeout(Duration(seconds: 5),
onTimeout: (() => throw Exception('Unable to enable WIP tab page')));
var runtime = connection.runtime;
await runtime.enable().timeout(Duration(seconds: 5),
onTimeout: (() => throw Exception('Unable to enable WIP runtime')));
var debugger = connection.debugger;
await debugger.enable().timeout(Duration(seconds: 5),
onTimeout: (() => throw Exception('Unable to enable WIP debugger')));
return ExpressionEvaluationTestDriver._(
chrome, chromeDir, connection, debugger);
chrome, chromeDir, connection, debugger, runtime);
}
/// Must be called when testing a new Dart program.
///
/// Depends on SDK artifacts (such as the sound and unsound dart_sdk.js
/// files) generated from the 'ddc_stable_test' and 'ddc_canary_test' targets.
Future<void> initSource(SetupCompilerOptions setup, String source,
{Map<String, bool> experiments = const {}}) async {
Future<void> initSource(
SetupCompilerOptions setup,
String source, {
Map<String, bool> experiments = const {},
}) async {
// Perform setup sanity checks.
var summaryPath = setup.options.sdkSummary!.toFilePath();
if (!File(summaryPath).existsSync()) {
@ -169,6 +180,7 @@ class ExpressionEvaluationTestDriver {
<script src='$outputPath'></script>
<script>
'use strict';
let dartApplication = true;
var sound = ${setup.soundNullSafety};
var sdk = dart_library.import('dart_sdk');
@ -216,6 +228,7 @@ class ExpressionEvaluationTestDriver {
},
waitSeconds: 15
});
let dartApplication = true;
var sound = ${setup.soundNullSafety};
require(['dart_sdk', '$moduleName'],
@ -272,7 +285,9 @@ class ExpressionEvaluationTestDriver {
Future<void> cleanupTest() async {
await setBreakpointsActive(debugger, false);
await debugger.removeBreakpoint(preemptiveBp);
if (preemptiveBp != null) {
await debugger.removeBreakpoint(preemptiveBp!);
}
setup.diagnosticMessages.clear();
setup.errors.clear();
}
@ -290,8 +305,8 @@ class ExpressionEvaluationTestDriver {
Future<wip.WipScript> _loadScript() async {
final scriptController = StreamController<wip.ScriptParsedEvent>();
final consoleSub =
debugger.connection.runtime.onConsoleAPICalled.listen(print);
final consoleSub = debugger.connection.runtime.onConsoleAPICalled
.listen((e) => printOnFailure('$e'));
// Fail on exceptions in JS code.
await debugger.setPauseOnExceptions(wip.PauseState.uncaught);
@ -330,6 +345,7 @@ class ExpressionEvaluationTestDriver {
}
}
/// Load the script and run [onPause] when the app pauses on [breakpointId].
Future<T> _onBreakpoint<T>(String breakpointId,
{required Future<T> Function(wip.DebuggerPausedEvent) onPause}) async {
// The next two pause events will correspond to:
@ -386,6 +402,22 @@ class ExpressionEvaluationTestDriver {
}
}
/// Load the script and run the [body] while the app is running.
Future<T> _whileRunning<T>({required Future<T> Function() body}) async {
final consoleSub = debugger.connection.runtime.onConsoleAPICalled
.listen((e) => printOnFailure('$e'));
await _loadScript();
try {
// Continue running, ignoring the first pause event since it corresponds
// to the preemptive URI breakpoint made prior to page navigation.
await debugger.resume();
return await body();
} finally {
await consoleSub.cancel();
}
}
Future<Map<String, String>> getScope(String breakpointId) async {
return await _onBreakpoint(breakpointId, onPause: (event) async {
// Retrieve the call frame and its scope variables.
@ -397,13 +429,13 @@ class ExpressionEvaluationTestDriver {
/// Evaluates a dart [expression] on a breakpoint.
///
/// [breakpointId] is the ID of the breakpoint from the source.
Future<String> evaluateDartExpression({
Future<String> evaluateDartExpressionInFrame({
required String breakpointId,
required String expression,
}) async {
var dartLine = _findBreakpointLine(breakpointId);
return await _onBreakpoint(breakpointId, onPause: (event) async {
var result = await _evaluateDartExpression(
var result = await _evaluateDartExpressionInFrame(
event,
expression,
dartLine,
@ -412,6 +444,14 @@ class ExpressionEvaluationTestDriver {
});
}
/// Evaluates a dart [expression] while the app is running.
Future<String> evaluateDartExpression({required String expression}) async {
return await _whileRunning(body: () async {
var result = await _evaluateDartExpression(expression);
return await stringifyRemoteObject(result);
});
}
/// Evaluates a js [expression] on a breakpoint.
///
/// [breakpointId] is the ID of the breakpoint from the source.
@ -456,7 +496,7 @@ class ExpressionEvaluationTestDriver {
/// [expression] is a dart expression.
/// [expectedResult] is the JSON for the returned remote object.
/// [expectedError] is the error string if the error is expected.
Future<void> check(
Future<void> checkInFrame(
{required String breakpointId,
required String expression,
dynamic expectedError,
@ -466,7 +506,7 @@ class ExpressionEvaluationTestDriver {
var dartLine = _findBreakpointLine(breakpointId);
return await _onBreakpoint(breakpointId, onPause: (event) async {
var evalResult = await _evaluateDartExpression(
var evalResult = await _evaluateDartExpressionInFrame(
event,
expression,
dartLine,
@ -493,6 +533,42 @@ class ExpressionEvaluationTestDriver {
});
}
/// Evaluates a dart [expression] without breakpoint and validates result.
///
/// [expression] is a dart expression.
/// [expectedResult] is the JSON for the returned remote object.
/// [expectedError] is the error string if the error is expected.
Future<void> check(
{required String expression,
dynamic expectedError,
dynamic expectedResult}) async {
assert(expectedError == null || expectedResult == null,
'Cannot expect both an error and result.');
return await _whileRunning(body: () async {
var evalResult = await _evaluateDartExpression(expression);
var error = evalResult.json['error'];
if (error != null) {
expect(
expectedError,
isNotNull,
reason: 'Unexpected expression evaluation failure:\n$error',
);
expect(error, _matches(expectedError!));
} else {
expect(
expectedResult,
isNotNull,
reason:
'Unexpected expression evaluation success:\n${evalResult.json}',
);
var actual = await stringifyRemoteObject(evalResult);
expect(actual, _matches(expectedResult!));
}
});
}
Future<wip.RemoteObject> _evaluateJsExpression(
wip.DebuggerPausedEvent event,
String expression, {
@ -521,41 +597,94 @@ class ExpressionEvaluationTestDriver {
);
}
Future<TestCompilationResult> _compileDartExpression(
Future<TestCompilationResult> _compileDartExpressionInFrame(
wip.WipCallFrame frame, String expression, int dartLine) async {
// Retrieve the call frame and its scope variables.
var scope = await _collectScopeVariables(frame);
// Perform an incremental compile.
return await compiler.compileExpression(
input: input,
line: dartLine,
column: 1,
scope: scope,
expression: expression);
input: input,
line: dartLine,
column: 1,
scope: scope,
expression: expression,
);
}
Future<wip.RemoteObject> _evaluateDartExpression(
Future<TestCompilationResult> _compileDartExpression(
String expression) async {
// Perform an incremental compile.
return await compiler.compileExpression(
input: input,
line: 1,
column: 1,
scope: {},
expression: expression,
);
}
Future<wip.RemoteObject> _evaluateDartExpressionInFrame(
wip.DebuggerPausedEvent event,
String expression,
int dartLine, {
bool returnByValue = false,
}) async {
var frame = event.getCallFrames().first;
var result = await _compileDartExpression(frame, expression, dartLine);
var result = await _compileDartExpressionInFrame(
frame,
expression,
dartLine,
);
if (!result.isSuccess) {
setup.diagnosticMessages.clear();
setup.errors.clear();
return wip.RemoteObject({'error': result.result});
return _createCompilationError(result);
}
// Evaluate the compiled expression.
return await debugger.evaluateOnCallFrame(
frame.callFrameId,
result.result!,
returnByValue: returnByValue,
);
try {
return await debugger.evaluateOnCallFrame(
frame.callFrameId,
result.result!,
returnByValue: returnByValue,
);
} on wip.ExceptionDetails catch (e) {
return _createRuntimeError(e);
}
}
Future<wip.RemoteObject> _evaluateDartExpression(
String expression, {
bool returnByValue = false,
}) async {
var result = await _compileDartExpression(expression);
if (!result.isSuccess) {
return _createCompilationError(result);
}
// Find the execution context for the dart app.
final context = await executionContext.id;
// Evaluate the compiled expression.
try {
return await runtime.evaluate(
result.result!,
contextId: context,
returnByValue: returnByValue,
);
} on wip.ExceptionDetails catch (e) {
return _createRuntimeError(e);
}
}
wip.RemoteObject _createCompilationError(TestCompilationResult result) {
setup.diagnosticMessages.clear();
setup.errors.clear();
return wip.RemoteObject({'error': result.result});
}
wip.RemoteObject _createRuntimeError(wip.ExceptionDetails error) {
return wip.RemoteObject({'error': error.exception!.description});
}
/// Generate simple string representation of a RemoteObject that closely
@ -664,6 +793,59 @@ class ExpressionEvaluationTestDriver {
}
}
/// The execution context in which to do remote evaluations.
///
/// Copied and simplified from webdev/dwds/lib/src/debugging/execution_context.dart.
class ExecutionContext {
static const _nextContextTimeoutDuration = Duration(milliseconds: 100);
final wip.WipRuntime _runtime;
/// Contexts that may contain a Dart application.
late StreamQueue<int> _contexts;
int? _id;
Future<int> get id async {
if (_id != null) return _id!;
while (await _contexts.hasNext.timeout(
_nextContextTimeoutDuration,
onTimeout: () => false,
)) {
final context = await _contexts.next;
printOnFailure('Trying context: $context');
try {
// Confirm the context belongs to a dart application.
final result = await _runtime.evaluate(
'dartApplication',
contextId: context,
returnByValue: true,
);
if (result.value != null) {
printOnFailure('Found dart app context: $context');
_id = context;
break;
}
} catch (_) {
printOnFailure('Failed context: $context, trying again...');
}
}
if (_id == null) {
throw StateError('No context with the running Dart application.');
}
return _id!;
}
ExecutionContext(this._runtime) {
final contextController = StreamController<int>();
_runtime.onExecutionContextsCleared.listen((_) => _id = null);
_runtime.onExecutionContextDestroyed.listen((_) => _id = null);
_runtime.onExecutionContextCreated
.listen((e) => contextController.add(e.id));
_contexts = StreamQueue(contextController.stream);
}
}
/// Filters the provided frame scopes to those that are pertinent for Dart
/// debugging.
///

View file

@ -417,7 +417,7 @@ void runSharedTests(
});
test('typeName (int type)', () async {
var typeName = await driver.evaluateDartExpression(
var typeName = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP',
expression: 'xType.toString()',
);
@ -425,7 +425,7 @@ void runSharedTests(
});
test('typeName (base type)', () async {
var typeName = await driver.evaluateDartExpression(
var typeName = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP',
expression: 'baseType.toString()',
);
@ -433,7 +433,7 @@ void runSharedTests(
});
test('getObjectMetadata (int type)', () async {
var typeName = await driver.evaluateDartExpression(
var typeName = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP',
expression: 'xType.toString()',
);
@ -452,7 +452,7 @@ void runSharedTests(
});
test('getObjectMetadata (base type)', () async {
var typeName = await driver.evaluateDartExpression(
var typeName = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP',
expression: 'baseType.toString()',
);
@ -471,7 +471,7 @@ void runSharedTests(
});
test('getObjectMetadata (type)', () async {
var typeName = await driver.evaluateDartExpression(
var typeName = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP',
expression: 'baseTypeType.toString()',
);
@ -490,7 +490,7 @@ void runSharedTests(
});
test('getObjectMetadata (Set type)', () async {
var typeName = await driver.evaluateDartExpression(
var typeName = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP',
expression: 'setType.toString()',
);
@ -509,7 +509,7 @@ void runSharedTests(
});
test('getObjectMetadata (List type)', () async {
var typeName = await driver.evaluateDartExpression(
var typeName = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP',
expression: 'listType.toString()',
);
@ -528,7 +528,7 @@ void runSharedTests(
});
test('getObjectMetadata (Map type)', () async {
var typeName = await driver.evaluateDartExpression(
var typeName = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP',
expression: 'mapType.toString()',
);