[dart2wasm] Fix context collection emptiness false positive.

In some cases, closure contexts were being left out of the parent chain
of their children because they were empty at the time the child closure
was created. If a usage appeared later in the visit of the parent, the
context would no longer be empty but the child would already be created
without a parent.

This was easiest to recreate in sync* function because unlike async, it
doesn't introduce hoisted helper variables (these immediately mark the
parent as non-empty).

In the attached bug the repro only happens with named parameters because
TFA transforms the named parameter into a Let that introduces a variable
before the closure with the usage in the let body after the closure. The
new test explicitly introduces the same pattern of a variable declared
before the closure and used after it.

The fix here is to not eagerly check for emptiness of the parents.
Instead we post-process the Contexts and relink the parent tree skipping
any empty nodes.

Bug: https://github.com/dart-lang/sdk/issues/63264
Change-Id: I2f75506b9fa879544b1a606d8f157fbd44ba8ce2
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/500680
Commit-Queue: Nate Biggs <natebiggs@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Nate Biggs
2026-05-07 09:21:19 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 6c377d447b
commit 0af31e37bb
2 changed files with 39 additions and 7 deletions
+16 -7
View File
@@ -1351,7 +1351,7 @@ class Context {
/// The parent of this context, corresponding to the lexically enclosing
/// owner. This is null if the context is a member context, or if all contexts
/// in the parent chain are skipped.
final Context? parent;
Context? parent;
/// The variables captured by this context.
final List<VariableDeclaration> variables = [];
@@ -1502,7 +1502,10 @@ class Closures {
void _collectContexts() {
if (captures.isNotEmpty || _isThisCaptured) {
_member.accept(_ContextCollector(this, translator.options.enableAsserts));
_ContextCollector(
this,
translator.options.enableAsserts,
).collect(_member);
}
}
@@ -1774,6 +1777,16 @@ class _ContextCollector extends RecursiveVisitor {
_ContextCollector(this.closures, this.enableAsserts);
void collect(Member member) {
member.accept(this);
for (final context in closures.contexts.values) {
while (context.parent?.isEmpty ?? false) {
context.parent = context.parent!.parent;
}
}
}
@override
void visitAssertStatement(AssertStatement node) {
if (enableAsserts) {
@@ -1793,12 +1806,8 @@ class _ContextCollector extends RecursiveVisitor {
currentContext == null ||
node.parent is Constructor && !isInInitializer;
Context? oldContext = currentContext;
Context? parent = currentContext;
while (parent != null && parent.isEmpty) {
parent = parent.parent;
}
bool containsThis = closures._isThisCaptured && outerMost;
currentContext = Context(node, parent, containsThis);
currentContext = Context(node, oldContext, containsThis);
closures.contexts[node] = currentContext!;
node.visitChildren(this);
currentContext = oldContext;
+23
View File
@@ -0,0 +1,23 @@
// Copyright (c) 2026, 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.
void main() {
print(another());
}
Iterable<Object?> another() sync* {
for (int i = 0; i < 1; i++) {
// Add another scope
yield Object();
}
// Declare i before the closure.
int i = 23;
yield test(() => [1]);
// Use i after the closure.
print(i);
}
Object? test(Iterable<Object?> Function() f) {
return f();
}