Skip to content

Commit

Permalink
Add variable support for new DDC async lowering (#2471)
Browse files Browse the repository at this point in the history
* Add variable support for new DDC async lowering.

---------

Co-authored-by: Nate Biggs <natebiggs@google.com>
  • Loading branch information
biggs0125 and natebiggs authored Aug 2, 2024
1 parent b9ca622 commit 645a068
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 24 deletions.
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

0 comments on commit 645a068

Please sign in to comment.