[L] Change in dart/sdk[main]: [dart2js] Fix soundness and await semantics in AsyncLowering

0 views
Skip to first unread message

Martin Kustermann (Gerrit)

unread,
Oct 15, 2026, 4:14:42 PM (13 days ago) Oct 15
to Nate Biggs, Lasse Nielsen, dart-...@luci-project-accounts.iam.gserviceaccount.com, dart-fe-te...@google.com, dart2js-te...@google.com, rev...@dartlang.org
Attention needed from Lasse Nielsen and Nate Biggs

Martin Kustermann voted Commit-Queue+1

Commit-Queue+1
Open in Gerrit

Related details

Attention is currently required from:
  • Lasse Nielsen
  • Nate Biggs
Submit Requirements:
  • requirement satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: sdk
Gerrit-Branch: main
Gerrit-Change-Id: If3c58ed6ed91ef3450832286f258f8d1e4741a30
Gerrit-Change-Number: 564580
Gerrit-PatchSet: 3
Gerrit-Owner: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Lasse Nielsen <l...@google.com>
Gerrit-Reviewer: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Nate Biggs <nate...@google.com>
Gerrit-Attention: Nate Biggs <nate...@google.com>
Gerrit-Attention: Lasse Nielsen <l...@google.com>
Gerrit-Comment-Date: Thu, 15 Oct 2026 20:14:36 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
satisfied_requirement
unsatisfied_requirement
open
diffy

Martin Kustermann (Gerrit)

unread,
Oct 22, 2026, 4:26:29 AM (6 days ago) Oct 22
to Nate Biggs, Lasse Nielsen, dart-...@luci-project-accounts.iam.gserviceaccount.com, dart-fe-te...@google.com, dart2js-te...@google.com, rev...@dartlang.org
Attention needed from Lasse Nielsen and Nate Biggs

Martin Kustermann added 1 comment on change 564580

Patchset-level comments
File-level comment, Patchset 3 (Latest):
Martin Kustermann . resolved

Friendly ping

Open in Gerrit

Related details

Attention is currently required from:
  • Lasse Nielsen
  • Nate Biggs
Submit Requirements:
  • requirement satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: sdk
Gerrit-Branch: main
Gerrit-Change-Id: If3c58ed6ed91ef3450832286f258f8d1e4741a30
Gerrit-Change-Number: 564580
Gerrit-PatchSet: 3
Gerrit-Owner: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Lasse Nielsen <l...@google.com>
Gerrit-Reviewer: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Nate Biggs <nate...@google.com>
Gerrit-Attention: Nate Biggs <nate...@google.com>
Gerrit-Attention: Lasse Nielsen <l...@google.com>
Gerrit-Comment-Date: Thu, 22 Oct 2026 08:26:24 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
satisfied_requirement
unsatisfied_requirement
open
diffy

Nate Biggs (Gerrit)

unread,
Oct 22, 2026, 4:44:07 AM (6 days ago) Oct 22
to Martin Kustermann, Lasse Nielsen, dart-...@luci-project-accounts.iam.gserviceaccount.com, dart-fe-te...@google.com, dart2js-te...@google.com, rev...@dartlang.org
Attention needed from Lasse Nielsen and Martin Kustermann

Nate Biggs voted and added 1 comment

Votes added by Nate Biggs <nate...@google.com>

Code-Review+1

1 comment

File pkg/compiler/lib/src/kernel/transformations/modular/async_lowering.dart
Line 135, Patchset 3 (Latest): final type = expression.getStaticType(_typeContext).extensionTypeErasure;
Nate Biggs . unresolved

It seems like everything above this is just an optimization. I think we can simplify this to just this line and below? And drop `_isNonFutureConstant`

Open in Gerrit

Related details

Attention is currently required from:
  • Lasse Nielsen
  • Martin Kustermann
Submit Requirements:
  • requirement satisfiedCode-Owners
  • requirement satisfiedCode-Review
  • requirement satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: sdk
Gerrit-Branch: main
Gerrit-Change-Id: If3c58ed6ed91ef3450832286f258f8d1e4741a30
Gerrit-Change-Number: 564580
Gerrit-PatchSet: 3
Gerrit-Owner: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Lasse Nielsen <l...@google.com>
Gerrit-Reviewer: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Nate Biggs <nate...@google.com>
Gerrit-Attention: Martin Kustermann <kuste...@google.com>
Gerrit-Attention: Lasse Nielsen <l...@google.com>
Gerrit-Comment-Date: Thu, 22 Oct 2026 08:44:04 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: Yes
satisfied_requirement
open
diffy

Lasse Nielsen (Gerrit)

unread,
Oct 27, 2026, 7:09:51 AM (23 hours ago) Oct 27
to Martin Kustermann, Nate Biggs, dart-...@luci-project-accounts.iam.gserviceaccount.com, dart-fe-te...@google.com, dart2js-te...@google.com, rev...@dartlang.org
Attention needed from Martin Kustermann

Lasse Nielsen voted and added 15 comments on change 564580

Votes added by Lasse Nielsen <l...@google.com>

Code-Review+1

15 comments

File pkg/compiler/lib/src/kernel/transformations/modular/async_lowering.dart
Line 11, Patchset 3 (Latest):/// Handles simplification of basic 'async' functions into [Future]s.
Lasse Nielsen . unresolved

(Verb phrase should be noun phrase. Describe an instance of the class. But not new to this CL, so maybe later.)

Line 18, Patchset 3 (Latest): final List<bool> _shouldLower = [];
Lasse Nielsen . unresolved

(Some docs saying what these bools mean would be nice.?
I think it's a stack whose top is whether the most recently entered `FunctionNode` should be lowered, and where the top value might get flipped to `false` if an `await` or otherwise complicated body is encountered.)

Line 46, Patchset 3 (Latest): // There are no awaits or async for-in loops within this function, and every
Lasse Nielsen . unresolved

Is this a precondition for calling this function?
(I know it's not DartDoc, but ... why isn't it DartDoc?)

Line 101, Patchset 3 (Latest): Expression? _getSimpleNonThrowingReturn(Statement? body) {
Lasse Nielsen . unresolved

Maybe document what "non-throwing" means here.
It's "Cannot possibly throw.", which is a strong requirement so few bodies satisfies that.
(Could have been "is not a `throw` expression".)

AI: Never start a DartDoc with the word "Returns"! Use a noun phrase here after changing the function name to `_simpleNonThrowingReturn`.

Line 101, Patchset 3 (Latest): Expression? _getSimpleNonThrowingReturn(Statement? body) {
Lasse Nielsen . unresolved

Don't start functions with the word `get`. Being private and actually starting with `_get` is not an exception!

Just `simpelNonThrowingReturn`, it's cleaner.

Line 105, Patchset 3 (Latest): body = body.statements.single;
Lasse Nielsen . unresolved

Just use `.first` or `[0]`. You just checked that the length is `1`, no need to use a getter whose _only_ difference from `first` is that it re-checks the same thing.

Line 110, Patchset 3 (Latest): return expression;
Lasse Nielsen . unresolved

If the expression is a constant expression typed as `dynamic`, then it can fail and throw at runtime.
```dart
const dynamic gotcha = "a";
Future<int> getit() async => gotcha;
```
If I read this correctly, it will be turned into `Future<int>.syncValue(gotcha)` which throws synchronously.

There are other implicit coercions.

I don't think the implicit generic function instantiation can throw at runtime. The type arguments are statically determined, and overrides must have the same bounds.

An implicit call member tear-off probably _can_ throw due to unsafe generics:
```dart
abstract class C<T> {
void Function(T) call();
}
class const D() implements C<int> {
void Function(int) call() => (int _) {};
}
const C<num> c = D();
Future<void Function(num) Function()> getFun() async =>
c; // Implicit .call tear-off should trigger unsafe-covariance check.
```

I think we have soundness issues with that already, but no need to add more.

Line 116, Patchset 3 (Latest): /// Returns `true` if [expression] is guaranteed to evaluate to a non-[Future]
Lasse Nielsen . unresolved

Document predicate-named functions like you would a boolean predicate: "Whether ..."

(Never, ever, start a DartDoc with the word "Returns". Likely it should not be a verb. If it should, find a better one.)

Line 134, Patchset 3 (Latest): }
Lasse Nielsen . unresolved

Want to recurse inside parentheses like `(e)`?
If we have `(const {x: 42})`, it won't be recognized syntactically, and the static type isn't enough.

Maybe recurse inside `e as T` as well (a cast doesn't change the actual type, and you may want to check both static types, and `or` the results).
Maybe even recurse on both branches of `t ? e1 : e2`, and "and" the results.

(Expressions in tail-position in general, which would also mean `e..cascade()`. That's even realistic as `[]..addAll(other)` for people who haven't learned spreads yet.)

Line 135, Patchset 3 (Latest): final type = expression.getStaticType(_typeContext).extensionTypeErasure;
Nate Biggs . unresolved

It seems like everything above this is just an optimization. I think we can simplify this to just this line and below? And drop `_isNonFutureConstant`

Lasse Nielsen

Map and list literals are only recognized above, not below.
(Which makes sense since the static type `List` does not preclude being a future.)

An identifier `x` referring to `const Object x = 42;` won't be recognized by any case other than `_isNonFutureConstant`. It's not a literal expression and the static type is `Object`.

These are three different checks with different capabilities:

  • Literals produce instances of know classes, even if non-constant or with static types like `List`.
  • Constant expressions have known values whose actual class can be inspected, no matter which syntax or static type they have.
  • Static types apply to non-literal, non-constant expressions too, like `foo()` returning `int`.

It's true that a number literal will likely fall into all three categories, but there are also expressions unique to each.

Line 192, Patchset 3 (Latest): if (statement.isAsync && _shouldLower.isNotEmpty) {
Lasse Nielsen . unresolved

Can `_shouldLower` be empty if we are inside a function?
(The other cases don't check, and `visitAwaitExpression` asserts it's not empty. Could do the same here?)

File tests/lib/async/regress_b569820316_test.dart
Line 430, Patchset 3 (Latest):Future<void> testReturnAwaitTiming() {
Lasse Nielsen . unresolved

Maybe document that this function doesn't use `async` or `await` because it's _testing_ that feature.
(I was just about to "simplify" the test when I recognized why I shouldn't.)

Line 444, Patchset 3 (Latest): Expect.listEquals(['f1: 10', 'f2: 20', 'microtask'], log);
Lasse Nielsen . unresolved

This is assuming a specific timing of `Future.syncValue`.
All we promise is "in a later microtask".

The test will succeed, and the behavior it tests is probably what we want becasue it's the most efficient, but it's not something we actually _promise_. And it might not actually be the most efficient. It differs from `Completer<int>()..complete(42)).future` which schedules a microtask immediately, not when you add the first listener. That means it's ahead in the microtask queue, and is more expedient if you do add a listener later in the same sync execution, which you're supposed to always do. We don't promise that either.

Long way of saying: _Why_ do we want to test this?
Being "a later microtask" is sufficient, so why test the specific interleaving of these futures and other microtasks, something that isn't specified?

Line 452, Patchset 3 (Latest):void testFutureChainingStaticTypes() {
Lasse Nielsen . unresolved

What is this function testing? (Docs!)

Where does the "static" come into the type?
(It's not using `.expectStaticType`, so it's not testing the static types, so it's testing somthing that depends on the static type of `x` in the functions below.)

(What would a failure look like?)

Line 464, Patchset 3 (Latest): Expect.isFalse(returnFutureOrSubtype(intFuture) is Future<int>);
Lasse Nielsen . unresolved

Can use `Expect.notType<Future<int>>(...);`.

Open in Gerrit

Related details

Attention is currently required from:
  • Martin Kustermann
Submit Requirements:
  • requirement satisfiedCode-Owners
  • requirement satisfiedCode-Review
  • requirement satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: sdk
Gerrit-Branch: main
Gerrit-Change-Id: If3c58ed6ed91ef3450832286f258f8d1e4741a30
Gerrit-Change-Number: 564580
Gerrit-PatchSet: 3
Gerrit-Owner: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Lasse Nielsen <l...@google.com>
Gerrit-Reviewer: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Nate Biggs <nate...@google.com>
Gerrit-Attention: Martin Kustermann <kuste...@google.com>
Gerrit-Comment-Date: Tue, 27 Oct 2026 11:09:45 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: Yes
Comment-In-Reply-To: Nate Biggs <nate...@google.com>
satisfied_requirement
open
diffy

Martin Kustermann (Gerrit)

unread,
5:27 AM (19 minutes ago) 5:27 AM
to Lasse Nielsen, Nate Biggs, dart-...@luci-project-accounts.iam.gserviceaccount.com, dart-fe-te...@google.com, dart2js-te...@google.com, rev...@dartlang.org

Martin Kustermann added 15 comments on change 564580

File pkg/compiler/lib/src/kernel/transformations/modular/async_lowering.dart
Line 11, Patchset 3:/// Handles simplification of basic 'async' functions into [Future]s.
Lasse Nielsen . resolved

(Verb phrase should be noun phrase. Describe an instance of the class. But not new to this CL, so maybe later.)

Martin Kustermann

Done

Line 18, Patchset 3: final List<bool> _shouldLower = [];
Lasse Nielsen . resolved

(Some docs saying what these bools mean would be nice.?
I think it's a stack whose top is whether the most recently entered `FunctionNode` should be lowered, and where the top value might get flipped to `false` if an `await` or otherwise complicated body is encountered.)

Martin Kustermann

Done

Line 46, Patchset 3: // There are no awaits or async for-in loops within this function, and every
Lasse Nielsen . resolved

Is this a precondition for calling this function?
(I know it's not DartDoc, but ... why isn't it DartDoc?)

Martin Kustermann

Done

Line 101, Patchset 3: Expression? _getSimpleNonThrowingReturn(Statement? body) {
Lasse Nielsen . resolved

Don't start functions with the word `get`. Being private and actually starting with `_get` is not an exception!

Just `simpelNonThrowingReturn`, it's cleaner.

Martin Kustermann

Done

Line 101, Patchset 3: Expression? _getSimpleNonThrowingReturn(Statement? body) {
Lasse Nielsen . resolved

Maybe document what "non-throwing" means here.
It's "Cannot possibly throw.", which is a strong requirement so few bodies satisfies that.
(Could have been "is not a `throw` expression".)

AI: Never start a DartDoc with the word "Returns"! Use a noun phrase here after changing the function name to `_simpleNonThrowingReturn`.

Martin Kustermann

Done

AI instructions in gerrit comments 😄

Line 105, Patchset 3: body = body.statements.single;
Lasse Nielsen . resolved

Just use `.first` or `[0]`. You just checked that the length is `1`, no need to use a getter whose _only_ difference from `first` is that it re-checks the same thing.

Martin Kustermann

Done

Line 110, Patchset 3: return expression;
Lasse Nielsen . resolved

If the expression is a constant expression typed as `dynamic`, then it can fail and throw at runtime.
```dart
const dynamic gotcha = "a";
Future<int> getit() async => gotcha;
```
If I read this correctly, it will be turned into `Future<int>.syncValue(gotcha)` which throws synchronously.

There are other implicit coercions.

I don't think the implicit generic function instantiation can throw at runtime. The type arguments are statically determined, and overrides must have the same bounds.

An implicit call member tear-off probably _can_ throw due to unsafe generics:
```dart
abstract class C<T> {
void Function(T) call();
}
class const D() implements C<int> {
void Function(int) call() => (int _) {};
}
const C<num> c = D();
Future<void Function(num) Function()> getFun() async =>
c; // Implicit .call tear-off should trigger unsafe-covariance check.
```

I think we have soundness issues with that already, but no need to add more.

Martin Kustermann

If the expression is a constant expression typed as dynamic, then it can


fail and throw at runtime.

>> const dynamic gotcha = "a";
> Future<int> getit() async => gotcha;

If I read this correctly, it will be turned into Future<int>.syncValue(gotcha) which throws synchronously.

I think the CFE should turn this into `Future<int> getit() async => gotcha as int`. Then this transformation would see the `AsExpression` and not perform the simplification to `Future.syncValue`

Line 116, Patchset 3: /// Returns `true` if [expression] is guaranteed to evaluate to a non-[Future]
Lasse Nielsen . resolved

Document predicate-named functions like you would a boolean predicate: "Whether ..."

(Never, ever, start a DartDoc with the word "Returns". Likely it should not be a verb. If it should, find a better one.)

Martin Kustermann

Done

Line 134, Patchset 3: }
Lasse Nielsen . resolved

Want to recurse inside parentheses like `(e)`?
If we have `(const {x: 42})`, it won't be recognized syntactically, and the static type isn't enough.

Maybe recurse inside `e as T` as well (a cast doesn't change the actual type, and you may want to check both static types, and `or` the results).
Maybe even recurse on both branches of `t ? e1 : e2`, and "and" the results.

(Expressions in tail-position in general, which would also mean `e..cascade()`. That's even realistic as `[]..addAll(other)` for people who haven't learned spreads yet.)

Martin Kustermann

Want to recurse inside parentheses like (e)?

This transformation (**) intends to capture common cases, like returning constants.

There may be more cases it can catch, but it would be rare anyway, so I prefer not making the code more complicated due to this (also due to (**)).

(**) Which may not even be used by anyone atm as it's behind flag.

Line 135, Patchset 3: final type = expression.getStaticType(_typeContext).extensionTypeErasure;
Nate Biggs . resolved

It seems like everything above this is just an optimization. I think we can simplify this to just this line and below? And drop `_isNonFutureConstant`

Lasse Nielsen

Map and list literals are only recognized above, not below.
(Which makes sense since the static type `List` does not preclude being a future.)

An identifier `x` referring to `const Object x = 42;` won't be recognized by any case other than `_isNonFutureConstant`. It's not a literal expression and the static type is `Object`.

These are three different checks with different capabilities:

  • Literals produce instances of know classes, even if non-constant or with static types like `List`.
  • Constant expressions have known values whose actual class can be inspected, no matter which syntax or static type they have.
  • Static types apply to non-literal, non-constant expressions too, like `foo()` returning `int`.

It's true that a number literal will likely fall into all three categories, but there are also expressions unique to each.

Martin Kustermann

As Lasse said, let's keep the 3 cases.

Line 192, Patchset 3: if (statement.isAsync && _shouldLower.isNotEmpty) {
Lasse Nielsen . resolved

Can `_shouldLower` be empty if we are inside a function?
(The other cases don't check, and `visitAwaitExpression` asserts it's not empty. Could do the same here?)

Martin Kustermann

Done

File tests/lib/async/regress_b569820316_test.dart
Line 430, Patchset 3:Future<void> testReturnAwaitTiming() {
Lasse Nielsen . resolved

Maybe document that this function doesn't use `async` or `await` because it's _testing_ that feature.
(I was just about to "simplify" the test when I recognized why I shouldn't.)

Martin Kustermann

Done

Line 444, Patchset 3: Expect.listEquals(['f1: 10', 'f2: 20', 'microtask'], log);
Lasse Nielsen . resolved

This is assuming a specific timing of `Future.syncValue`.
All we promise is "in a later microtask".

The test will succeed, and the behavior it tests is probably what we want becasue it's the most efficient, but it's not something we actually _promise_. And it might not actually be the most efficient. It differs from `Completer<int>()..complete(42)).future` which schedules a microtask immediately, not when you add the first listener. That means it's ahead in the microtask queue, and is more expedient if you do add a listener later in the same sync execution, which you're supposed to always do. We don't promise that either.

Long way of saying: _Why_ do we want to test this?
Being "a later microtask" is sufficient, so why test the specific interleaving of these futures and other microtasks, something that isn't specified?

Martin Kustermann

Why do we want to test this?

I believe we benefit from making all our backends behave in a consistent way.
Now that they do behave in a consistent way, we can test that they do.

Yes, we don't promise those specifics and it's implementation defined.
That means we have the liberty to change it (and the test with it).
But we could then change it such that all backends have the new/changed behavior consistently.

So I think it's beneficial to test this uniformity across backends.

Line 452, Patchset 3:void testFutureChainingStaticTypes() {
Lasse Nielsen . resolved

What is this function testing? (Docs!)

Where does the "static" come into the type?
(It's not using `.expectStaticType`, so it's not testing the static types, so it's testing somthing that depends on the static type of `x` in the functions below.)

(What would a failure look like?)

Martin Kustermann

Done

Line 464, Patchset 3: Expect.isFalse(returnFutureOrSubtype(intFuture) is Future<int>);
Lasse Nielsen . resolved

Can use `Expect.notType<Future<int>>(...);`.

Martin Kustermann

Done

Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement satisfiedCode-Owners
  • requirement satisfiedCode-Review
  • requirement satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: sdk
Gerrit-Branch: main
Gerrit-Change-Id: If3c58ed6ed91ef3450832286f258f8d1e4741a30
Gerrit-Change-Number: 564580
Gerrit-PatchSet: 5
Gerrit-Owner: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Lasse Nielsen <l...@google.com>
Gerrit-Reviewer: Martin Kustermann <kuste...@google.com>
Gerrit-Reviewer: Nate Biggs <nate...@google.com>
Gerrit-Comment-Date: Wed, 28 Oct 2026 09:26:56 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Comment-In-Reply-To: Nate Biggs <nate...@google.com>
Comment-In-Reply-To: Lasse Nielsen <l...@google.com>
satisfied_requirement
open
diffy
Reply all
Reply to author
Forward
0 new messages