Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add variable support for new DDC async lowering #2471

Merged
merged 2 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions dwds/lib/src/debugging/dart_scope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ final ddcTemporaryTypeVariableRegExp = RegExp(r'^__t[\$\w*]+$');
final previousDdcTemporaryVariableRegExp =
RegExp(r'^(t[0-9]+\$?[0-9]*|__t[\$\w*]+)$');

const ddcAsyncScope = 'asyncScope';
const ddcCapturedAsyncScope = 'capturedAsyncScope';

/// Find the visible Dart variables from a JS Scope Chain, coming from the
/// scopeChain attribute of a Chrome CallFrame corresponding to [frame].
///
Expand Down Expand Up @@ -70,6 +73,46 @@ Future<List<Property>> visibleVariables({
);
}

// DDC's async lowering hoists variable declarations into scope objects. We
// create one scope object per Dart scope (skipping scopes containing no
// declarations). If a Dart scope is captured by a Dart closure the
// JS scope object will also be captured by the compiled JS closure.
//
// For debugging purposes we unpack these scope objects into the set of
// available properties to recreate the Dart context at any given point.

final capturedAsyncScopes = [
...allProperties
.where((p) => p.name?.startsWith(ddcCapturedAsyncScope) ?? false),
];

if (capturedAsyncScopes.isNotEmpty) {
// If we are in a local function within an async function, we should use the
// available captured scopes. These will contain all the variables captured
// by the closure. We only close over variables used within the closure.
for (final scopeObject in capturedAsyncScopes) {
final scopeObjectId = scopeObject.value?.objectId;
if (scopeObjectId == null) continue;
final scopeProperties = await inspector.getProperties(scopeObjectId);
allProperties.addAll(scopeProperties);
allProperties.remove(scopeObject);
}
} else {
// Otherwise we are in the async function body itself. Unpack the available
// async scopes. Scopes we have not entered may already have a scope object
// declared but the object will not have any values in it yet.
final asyncScopes = [
...allProperties.where((p) => p.name?.startsWith(ddcAsyncScope) ?? false),
];
for (final scopeObject in asyncScopes) {
final scopeObjectId = scopeObject.value?.objectId;
if (scopeObjectId == null) continue;
final scopeProperties = await inspector.getProperties(scopeObjectId);
allProperties.addAll(scopeProperties);
allProperties.remove(scopeObject);
}
}

allProperties.removeWhere((property) {
final value = property.value;
if (value == null) return true;
Expand Down
82 changes: 58 additions & 24 deletions dwds/test/variable_scope_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -187,30 +187,64 @@ void main() {
expect(variableNames, containsAll(['formal']));
});

test(
'variables in function',
() async {
stack = await breakAt('nestedFunction', mainScript);
final variables = getFrameVariables(stack.frames!.first);
await expectDartVariables(variables);

final variableNames = variables.keys.toList()..sort();
expect(
variableNames,
containsAll([
'aClass',
'another',
'intLocalInMain',
'local',
'localThatsNull',
'nestedFunction',
'parameter',
'testClass',
]),
);
},
skip: 'See https://github.com/dart-lang/webdev/issues/2469',
);
test('variables in static async function', () async {
stack = await breakAt('staticAsyncFunction', mainScript);
final variables = getFrameVariables(stack.frames!.first);
await expectDartVariables(variables);

final variableNames = variables.keys.toList()..sort();
final variableValues =
variableNames.map((name) => variables[name]?.valueAsString).toList();
expect(
variableNames,
containsAll(['myLocal', 'value']),
);
expect(
variableValues,
containsAll(['a local value', 'arg1']),
);
});

test('variables in static async loop function', () async {
stack = await breakAt('staticAsyncLoopFunction', mainScript);
final variables = getFrameVariables(stack.frames!.first);
await expectDartVariables(variables);

final variableNames = variables.keys.toList()..sort();
final variableValues =
variableNames.map((name) => variables[name]?.valueAsString).toList();
expect(
variableNames,
containsAll(['i', 'myLocal', 'value']),
);
// Ensure the loop variable, i, is captued correctly. The value from the
// first iteration should be captured by the saved closure.
expect(
variableValues,
containsAll(['1', 'my local value', 'arg2']),
);
});

test('variables in function', () async {
stack = await breakAt('nestedFunction', mainScript);
final variables = getFrameVariables(stack.frames!.first);
await expectDartVariables(variables);

final variableNames = variables.keys.toList()..sort();
expect(
variableNames,
containsAll([
'aClass',
'another',
'intLocalInMain',
'local',
'localThatsNull',
'nestedFunction',
'parameter',
'testClass',
]),
);
});

test('variables in closure nested in method', () async {
stack = await breakAt('nestedClosure', mainScript);
Expand Down
21 changes: 21 additions & 0 deletions fixtures/_testSound/example/scopes/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ void staticFunction(int formal) {
print(formal); // Breakpoint: staticFunction
}

void staticAsyncFunction(String value) async {
var myLocal = await 'a local value';
print(value); // Breakpoint: staticAsyncFunction
}

void staticAsyncLoopFunction(String value) async {
Function? f;
for (var i in [1, 2, 3]) {
print(i);
var myLocal = await 'my local value';
f ??= () {
print(value);
print(i);
return myLocal; // Breakpoint: staticAsyncLoopFunction
};
}
f!();
}

void main() async {
print('Initial print from scopes app');
var local = 'local in main';
Expand Down Expand Up @@ -52,6 +71,8 @@ void main() async {
var closureLocal;
libraryPublicFinal.printCount();
staticFunction(1);
staticAsyncFunction('arg1');
staticAsyncLoopFunction('arg2');
print('ticking... $ticks (the answer is $intLocalInMain)');
print(nestedFunction('$ticks ${testClass.message}', Timer));
print(localThatsNull);
Expand Down
Loading