Skip to content

Commit 645a068

Browse files
biggs0125natebiggs
andauthored
Add variable support for new DDC async lowering (#2471)
* Add variable support for new DDC async lowering. --------- Co-authored-by: Nate Biggs <[email protected]>
1 parent b9ca622 commit 645a068

File tree

3 files changed

+122
-24
lines changed

3 files changed

+122
-24
lines changed

dwds/lib/src/debugging/dart_scope.dart

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ final ddcTemporaryTypeVariableRegExp = RegExp(r'^__t[\$\w*]+$');
2828
final previousDdcTemporaryVariableRegExp =
2929
RegExp(r'^(t[0-9]+\$?[0-9]*|__t[\$\w*]+)$');
3030

31+
const ddcAsyncScope = 'asyncScope';
32+
const ddcCapturedAsyncScope = 'capturedAsyncScope';
33+
3134
/// Find the visible Dart variables from a JS Scope Chain, coming from the
3235
/// scopeChain attribute of a Chrome CallFrame corresponding to [frame].
3336
///
@@ -70,6 +73,46 @@ Future<List<Property>> visibleVariables({
7073
);
7174
}
7275

76+
// DDC's async lowering hoists variable declarations into scope objects. We
77+
// create one scope object per Dart scope (skipping scopes containing no
78+
// declarations). If a Dart scope is captured by a Dart closure the
79+
// JS scope object will also be captured by the compiled JS closure.
80+
//
81+
// For debugging purposes we unpack these scope objects into the set of
82+
// available properties to recreate the Dart context at any given point.
83+
84+
final capturedAsyncScopes = [
85+
...allProperties
86+
.where((p) => p.name?.startsWith(ddcCapturedAsyncScope) ?? false),
87+
];
88+
89+
if (capturedAsyncScopes.isNotEmpty) {
90+
// If we are in a local function within an async function, we should use the
91+
// available captured scopes. These will contain all the variables captured
92+
// by the closure. We only close over variables used within the closure.
93+
for (final scopeObject in capturedAsyncScopes) {
94+
final scopeObjectId = scopeObject.value?.objectId;
95+
if (scopeObjectId == null) continue;
96+
final scopeProperties = await inspector.getProperties(scopeObjectId);
97+
allProperties.addAll(scopeProperties);
98+
allProperties.remove(scopeObject);
99+
}
100+
} else {
101+
// Otherwise we are in the async function body itself. Unpack the available
102+
// async scopes. Scopes we have not entered may already have a scope object
103+
// declared but the object will not have any values in it yet.
104+
final asyncScopes = [
105+
...allProperties.where((p) => p.name?.startsWith(ddcAsyncScope) ?? false),
106+
];
107+
for (final scopeObject in asyncScopes) {
108+
final scopeObjectId = scopeObject.value?.objectId;
109+
if (scopeObjectId == null) continue;
110+
final scopeProperties = await inspector.getProperties(scopeObjectId);
111+
allProperties.addAll(scopeProperties);
112+
allProperties.remove(scopeObject);
113+
}
114+
}
115+
73116
allProperties.removeWhere((property) {
74117
final value = property.value;
75118
if (value == null) return true;

dwds/test/variable_scope_test.dart

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -187,30 +187,64 @@ void main() {
187187
expect(variableNames, containsAll(['formal']));
188188
});
189189

190-
test(
191-
'variables in function',
192-
() async {
193-
stack = await breakAt('nestedFunction', mainScript);
194-
final variables = getFrameVariables(stack.frames!.first);
195-
await expectDartVariables(variables);
196-
197-
final variableNames = variables.keys.toList()..sort();
198-
expect(
199-
variableNames,
200-
containsAll([
201-
'aClass',
202-
'another',
203-
'intLocalInMain',
204-
'local',
205-
'localThatsNull',
206-
'nestedFunction',
207-
'parameter',
208-
'testClass',
209-
]),
210-
);
211-
},
212-
skip: 'See https://github.com/dart-lang/webdev/issues/2469',
213-
);
190+
test('variables in static async function', () async {
191+
stack = await breakAt('staticAsyncFunction', mainScript);
192+
final variables = getFrameVariables(stack.frames!.first);
193+
await expectDartVariables(variables);
194+
195+
final variableNames = variables.keys.toList()..sort();
196+
final variableValues =
197+
variableNames.map((name) => variables[name]?.valueAsString).toList();
198+
expect(
199+
variableNames,
200+
containsAll(['myLocal', 'value']),
201+
);
202+
expect(
203+
variableValues,
204+
containsAll(['a local value', 'arg1']),
205+
);
206+
});
207+
208+
test('variables in static async loop function', () async {
209+
stack = await breakAt('staticAsyncLoopFunction', mainScript);
210+
final variables = getFrameVariables(stack.frames!.first);
211+
await expectDartVariables(variables);
212+
213+
final variableNames = variables.keys.toList()..sort();
214+
final variableValues =
215+
variableNames.map((name) => variables[name]?.valueAsString).toList();
216+
expect(
217+
variableNames,
218+
containsAll(['i', 'myLocal', 'value']),
219+
);
220+
// Ensure the loop variable, i, is captued correctly. The value from the
221+
// first iteration should be captured by the saved closure.
222+
expect(
223+
variableValues,
224+
containsAll(['1', 'my local value', 'arg2']),
225+
);
226+
});
227+
228+
test('variables in function', () async {
229+
stack = await breakAt('nestedFunction', mainScript);
230+
final variables = getFrameVariables(stack.frames!.first);
231+
await expectDartVariables(variables);
232+
233+
final variableNames = variables.keys.toList()..sort();
234+
expect(
235+
variableNames,
236+
containsAll([
237+
'aClass',
238+
'another',
239+
'intLocalInMain',
240+
'local',
241+
'localThatsNull',
242+
'nestedFunction',
243+
'parameter',
244+
'testClass',
245+
]),
246+
);
247+
});
214248

215249
test('variables in closure nested in method', () async {
216250
stack = await breakAt('nestedClosure', mainScript);

fixtures/_testSound/example/scopes/main.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,25 @@ void staticFunction(int formal) {
2525
print(formal); // Breakpoint: staticFunction
2626
}
2727

28+
void staticAsyncFunction(String value) async {
29+
var myLocal = await 'a local value';
30+
print(value); // Breakpoint: staticAsyncFunction
31+
}
32+
33+
void staticAsyncLoopFunction(String value) async {
34+
Function? f;
35+
for (var i in [1, 2, 3]) {
36+
print(i);
37+
var myLocal = await 'my local value';
38+
f ??= () {
39+
print(value);
40+
print(i);
41+
return myLocal; // Breakpoint: staticAsyncLoopFunction
42+
};
43+
}
44+
f!();
45+
}
46+
2847
void main() async {
2948
print('Initial print from scopes app');
3049
var local = 'local in main';
@@ -52,6 +71,8 @@ void main() async {
5271
var closureLocal;
5372
libraryPublicFinal.printCount();
5473
staticFunction(1);
74+
staticAsyncFunction('arg1');
75+
staticAsyncLoopFunction('arg2');
5576
print('ticking... $ticks (the answer is $intLocalInMain)');
5677
print(nestedFunction('$ticks ${testClass.message}', Timer));
5778
print(localThatsNull);

0 commit comments

Comments
 (0)