unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
error-handlingapi-designtesting-strategy

[clang] Fix lost lambda capture in dependent co_return operand

Project: llvm/llvm-project | Language: C++ | Concepts: error-handling, api-design, testing-strategy | Difficulty: senior
Link: https://github.com/llvm/llvm-project/pull/220234

The problem

When using a coroutine with a type-dependent co_return operand inside a lambda, Clang would fail to capture variables from the enclosing scope if those variables were only referenced in the dependent operand. This led to valid code being rejected with an error like "reference to local variable 'p' declared in enclosing function".

Why it's non-trivial

The naive approach would be to simply return early when encountering a type-dependent operand, deferring further processing until instantiation. However, in Clang's lambda implementation, capturing variables from enclosing scopes is a two-step process: first, marking them as potential captures, and then finalizing the capture when the full expression is finished. Skipping the full-expression finalization step for dependent operands means the capture never happens, causing subtle bugs that only appear in template/generic lambda contexts.

The approach

The fix ensures that, even for type-dependent operands, the full-expression finalization is performed, so that any potential captures are realized as actual captures in the lambda. Here are the key code changes:

-  if (E && !isa<InitListExpr>(E) && E->isTypeDependent())
-    return new (Context)
-        CoreturnStmt(Loc, E, /*PromiseCall=*/nullptr, IsImplicit);
+  if (E && !isa<InitListExpr>(E) && E->isTypeDependent()) {
+    // Still finish the full-expression, so that potential captures in the
+    // operand are turned into actual captures of the enclosing lambda.
+    ExprResult FE = ActOnFinishFullExpr(E, /*DiscardedValue=*/false);
+    if (FE.isInvalid())
+      return StmtError();
+    return new (Context)
+        CoreturnStmt(Loc, FE.get(), /*PromiseCall=*/nullptr, IsImplicit);
+  }

What this does:

  • Previously, the function returned immediately for type-dependent operands, skipping ActOnFinishFullExpr.
  • The new code wraps the early return in a block that first calls ActOnFinishFullExpr on the operand, ensuring that any potential captures are finalized.
  • If the full expression is invalid, it returns an error; otherwise, it constructs the CoreturnStmt with the finalized expression.

A new test was also added to verify this behavior:

+void captured_by_dependent_coreturn(int p) {
+  call([&](auto &t) -> coro_void {
+    if (t)
+      co_return;
+    co_return use_void(p, t);
+  });
+  call([&](auto &t) -> coro_value {
+    if (t)
+      co_return 0;
+    co_return use_value(p, t);
+  });
+}

What this does:

  • Tests that a variable (p) from the enclosing function is correctly captured by the lambda, even when only used in a type-dependent co_return operand.

From the review thread

No notable review discussion on this PR.

Takeaways

  • Always finalize full expressions in semantic analysis, even for dependent expressions, to ensure correct side effects like lambda captures.
  • Be wary of early returns in compiler logic that may skip essential steps, especially in template/generic contexts.
  • When fixing subtle language implementation bugs, add targeted regression tests that mimic real-world usage patterns to prevent future regressions.