From 0af31e37bb84608f36d2474d6ea6a239e0c200a9 Mon Sep 17 00:00:00 2001 From: Nate Biggs Date: Thu, 7 May 2026 09:21:19 -0700 Subject: [PATCH] [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 Reviewed-by: Martin Kustermann --- pkg/dart2wasm/lib/closures.dart | 23 ++++++++++++++++------- tests/web/wasm/regress_63264_test.dart | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 tests/web/wasm/regress_63264_test.dart diff --git a/pkg/dart2wasm/lib/closures.dart b/pkg/dart2wasm/lib/closures.dart index 7093e41c142..1fc24c7631a 100644 --- a/pkg/dart2wasm/lib/closures.dart +++ b/pkg/dart2wasm/lib/closures.dart @@ -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 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; diff --git a/tests/web/wasm/regress_63264_test.dart b/tests/web/wasm/regress_63264_test.dart new file mode 100644 index 00000000000..cc840003e1c --- /dev/null +++ b/tests/web/wasm/regress_63264_test.dart @@ -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 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 Function() f) { + return f(); +}