From c2b39636f238c03385e2e484847d43811ce0d2ee Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sun, 20 Sep 2026 16:57:04 -0400 Subject: [PATCH 01/16] Skip cleanup when GetAsyncEnumerator returned null, rather than throwing a second confounding exception --- standard/statements.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/standard/statements.md b/standard/statements.md index 083d0b7cc..e6edda718 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -1388,7 +1388,8 @@ In the case where the expression `enumerable` represents a method call expressio The body of the `finally` block is constructed according to the following steps: -- If `E` has an accessible `DisposeAsync()` method where the return type is awaitable ([§12.9.9.2](expressions.md#12992-awaitable-expressions)), the `finally` clause is expanded to the semantic equivalent of: +- If `E` has an accessible `DisposeAsync()` method where the return type is awaitable ([§12.9.9.2](expressions.md#12992-awaitable-expressions)), then + - If `E` is a non-nullable value type then the `finally` clause is expanded to the semantic equivalent of: ```csharp finally @@ -1397,6 +1398,18 @@ The body of the `finally` block is constructed according to the following steps: } ``` + - Otherwise the `finally` clause is expanded to the semantic equivalent of: + + ```csharp + finally + { + if ((object)e != null) + { + await e.DisposeAsync(); + } + } + ``` + - Otherwise, if there is an implicit conversion from `E` to the `System.IAsyncDisposable` interface and `E` is a non-nullable value type then the `finally` clause is expanded to the semantic equivalent of: ```csharp From c47a1ec4a941dca857df46d543574fec51d0d343 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sun, 20 Sep 2026 17:00:31 -0400 Subject: [PATCH 02/16] There is a compile error rather than a fallback if DisposeAsync can't be awaited --- standard/statements.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/standard/statements.md b/standard/statements.md index e6edda718..bac6e9a61 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -1388,8 +1388,9 @@ In the case where the expression `enumerable` represents a method call expressio The body of the `finally` block is constructed according to the following steps: -- If `E` has an accessible `DisposeAsync()` method where the return type is awaitable ([§12.9.9.2](expressions.md#12992-awaitable-expressions)), then - - If `E` is a non-nullable value type then the `finally` clause is expanded to the semantic equivalent of: +- If `E` has an accessible `DisposeAsync()` method, then + - If the return type is not awaitable ([§12.9.9.2](expressions.md#12992-awaitable-expressions)), an error is produced and no further steps are taken. + - Otherwise, if `E` is a non-nullable value type then the `finally` clause is expanded to the semantic equivalent of: ```csharp finally From fb3b2f2c8a4e4f79cf56a7fb490529db7766e294 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sun, 20 Sep 2026 17:07:27 -0400 Subject: [PATCH 03/16] Async enumerators can't be ref structs (not only is this blocked by v8 rules, but impossible at the runtime level) --- standard/statements.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/standard/statements.md b/standard/statements.md index bac6e9a61..9d59f73a8 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -1421,15 +1421,6 @@ The body of the `finally` block is constructed according to the following steps: ``` except that if `E` is a value type, or a type parameter instantiated to a value type, then the conversion of `e` to `System.IAsyncDisposable` shall not cause boxing to occur. -- Otherwise, if `E` is a `ref struct` type and has an accessible `Dispose()` method, the `finally` clause is expanded to the semantic equivalent of: - - ```csharp - finally - { - e.Dispose(); - } - ``` - - Otherwise, if `E` is a sealed type, the `finally` clause is expanded to an empty block: ```csharp From 0213618036575fbc17cc8717f3f62ae45dbf6ccd Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sun, 20 Sep 2026 17:20:02 -0400 Subject: [PATCH 04/16] Invert the condition to swap the positions of the last two blocks --- standard/statements.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/standard/statements.md b/standard/statements.md index 9d59f73a8..39dd470dd 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -1421,13 +1421,7 @@ The body of the `finally` block is constructed according to the following steps: ``` except that if `E` is a value type, or a type parameter instantiated to a value type, then the conversion of `e` to `System.IAsyncDisposable` shall not cause boxing to occur. -- Otherwise, if `E` is a sealed type, the `finally` clause is expanded to an empty block: - - ```csharp - finally {} - ``` - -- Otherwise, the `finally` clause is expanded to: +- Otherwise, if `E` is not a sealed type, the `finally` clause is expanded to: ```csharp finally @@ -1440,7 +1434,13 @@ The body of the `finally` block is constructed according to the following steps: } ``` -The local variable `d` is not visible to or accessible to any user code. In particular, it does not conflict with any other variable whose scope includes the `finally` block. + The local variable `d` is not visible to or accessible to any user code. In particular, it does not conflict with any other variable whose scope includes the `finally` block. + +- Otherwise, the `finally` clause is expanded to an empty block: + + ```csharp + finally {} + ``` > *Note*: An `await foreach` is not required to dispose of `e` synchronously if an asynchronous dispose mechanism is not available. *end note* From 27a26651d0185fd3c2aff07baa1f992820711131 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sun, 20 Sep 2026 17:41:40 -0400 Subject: [PATCH 05/16] IAsyncDisposable.DisposeAsync *is* called on sealed reference types and it's not dynamically checked on unsealed types --- standard/statements.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/standard/statements.md b/standard/statements.md index 39dd470dd..94b457747 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -1411,7 +1411,8 @@ The body of the `finally` block is constructed according to the following steps: } ``` -- Otherwise, if there is an implicit conversion from `E` to the `System.IAsyncDisposable` interface and `E` is a non-nullable value type then the `finally` clause is expanded to the semantic equivalent of: +- Otherwise, if there is an implicit conversion from `E` to the `System.IAsyncDisposable` interface, then + - If `E` is a non-nullable value type then the `finally` clause is expanded to the semantic equivalent of: ```csharp finally @@ -1420,21 +1421,19 @@ The body of the `finally` block is constructed according to the following steps: } ``` - except that if `E` is a value type, or a type parameter instantiated to a value type, then the conversion of `e` to `System.IAsyncDisposable` shall not cause boxing to occur. -- Otherwise, if `E` is not a sealed type, the `finally` clause is expanded to: + - Otherwise the `finally` clause is expanded to the semantic equivalent of: - ```csharp - finally - { - System.IAsyncDisposable d = e as System.IAsyncDisposable; - if (d != null) - { - await d.DisposeAsync(); - } - } - ``` + ```csharp + finally + { + if ((object)e != null) + { + await ((System.IAsyncDisposable)e).DisposeAsync(); + } + } + ``` - The local variable `d` is not visible to or accessible to any user code. In particular, it does not conflict with any other variable whose scope includes the `finally` block. + If `E` is a value type, or a type parameter instantiated to a value type, then the conversion of `e` to `System.IAsyncDisposable` shall not cause boxing to occur. - Otherwise, the `finally` clause is expanded to an empty block: From 33e9f9b45bb191dd5c5b2df3aac318af26462fa3 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sun, 20 Sep 2026 17:48:50 -0400 Subject: [PATCH 06/16] Null checks on non-nullable things is semanically equivalent to doing nothing --- standard/statements.md | 43 ++++++++++++++---------------------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/standard/statements.md b/standard/statements.md index 94b457747..672bc9988 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -1390,15 +1390,6 @@ The body of the `finally` block is constructed according to the following steps: - If `E` has an accessible `DisposeAsync()` method, then - If the return type is not awaitable ([§12.9.9.2](expressions.md#12992-awaitable-expressions)), an error is produced and no further steps are taken. - - Otherwise, if `E` is a non-nullable value type then the `finally` clause is expanded to the semantic equivalent of: - - ```csharp - finally - { - await e.DisposeAsync(); - } - ``` - - Otherwise the `finally` clause is expanded to the semantic equivalent of: ```csharp @@ -1411,27 +1402,17 @@ The body of the `finally` block is constructed according to the following steps: } ``` -- Otherwise, if there is an implicit conversion from `E` to the `System.IAsyncDisposable` interface, then - - If `E` is a non-nullable value type then the `finally` clause is expanded to the semantic equivalent of: +- Otherwise, if there is an implicit conversion from `E` to the `System.IAsyncDisposable` interface, the `finally` clause is expanded to the semantic equivalent of: - ```csharp - finally - { - await ((System.IAsyncDisposable)e).DisposeAsync(); - } - ``` - - - Otherwise the `finally` clause is expanded to the semantic equivalent of: - - ```csharp - finally - { - if ((object)e != null) - { - await ((System.IAsyncDisposable)e).DisposeAsync(); - } - } - ``` + ```csharp + finally + { + if ((object)e != null) + { + await ((System.IAsyncDisposable)e).DisposeAsync(); + } + } + ``` If `E` is a value type, or a type parameter instantiated to a value type, then the conversion of `e` to `System.IAsyncDisposable` shall not cause boxing to occur. @@ -1441,6 +1422,10 @@ The body of the `finally` block is constructed according to the following steps: finally {} ``` +> *Note*: When `E` is a non-nullable value type, the null checks shown above are elided. *end note* + + + > *Note*: An `await foreach` is not required to dispose of `e` synchronously if an asynchronous dispose mechanism is not available. *end note* #### 13.9.5.4 Deconstructing foreach From 2eee31504fc40556b92d92529e5e155dff89d447 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sun, 20 Sep 2026 20:07:44 -0400 Subject: [PATCH 07/16] Ambiguous DisposeAsync lookup causes fallback to interface, and static DisposeAsync should not be found --- standard/statements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard/statements.md b/standard/statements.md index 672bc9988..ab2282a7c 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -1388,7 +1388,7 @@ In the case where the expression `enumerable` represents a method call expressio The body of the `finally` block is constructed according to the following steps: -- If `E` has an accessible `DisposeAsync()` method, then +- Perform member lookup ([§12.5](expressions.md#125-member-lookup)) on `E` with the identifier `DisposeAsync` and no type arguments. If the result is a method group and overload resolution ([§12.6.4](expressions.md#1264-overload-resolution)) with an empty argument list selects an accessible instance method, that method is selected for asynchronous disposal: - If the return type is not awaitable ([§12.9.9.2](expressions.md#12992-awaitable-expressions)), an error is produced and no further steps are taken. - Otherwise the `finally` clause is expanded to the semantic equivalent of: From d38c0b89cd9416a7c824e84db1604a2f7d68c81d Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sun, 20 Sep 2026 18:56:38 -0400 Subject: [PATCH 08/16] Add note that there is no DisposeAsync lookup on nullable underlying value type --- standard/statements.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/standard/statements.md b/standard/statements.md index ab2282a7c..721c2e29b 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -1402,6 +1402,8 @@ The body of the `finally` block is constructed according to the following steps: } ``` + > *Note*: If `E` is a nullable value type ([§8.3.12](types.md#8312-nullable-value-types)), member lookup for `DisposeAsync` is performed on `E`, not on its underlying type. *end note* + - Otherwise, if there is an implicit conversion from `E` to the `System.IAsyncDisposable` interface, the `finally` clause is expanded to the semantic equivalent of: ```csharp From c1eb16e5bf4b1b621f3fc614d1303e37f193bfc0 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sun, 20 Sep 2026 18:23:11 -0400 Subject: [PATCH 09/16] Typo (DisposeAsync returns ValueTask) --- standard/statements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard/statements.md b/standard/statements.md index 721c2e29b..65155e519 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -2173,7 +2173,7 @@ When `ResourceType` is a reference type that implements `IAsyncDisposable`. Othe await using (ResourceType resource = «expression») «statement» ``` -is semantically equivalent to the formulations shown below with `IAsyncDisposable` instead of `IDisposable`, `DisposeAsync` instead of `Dispose`, and the `Task` returned from `DisposeAsync` is `await`ed: +is semantically equivalent to the formulations shown below with `IAsyncDisposable` instead of `IDisposable`, `DisposeAsync` instead of `Dispose`, and the `ValueTask` returned from `DisposeAsync` is `await`ed: ```csharp await using (ResourceType resource = «expression») «statement» From 20d921cdb15121d689387f70eb5bb40f4b5729a1 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sun, 20 Sep 2026 20:03:45 -0400 Subject: [PATCH 10/16] 'await using' prefers an non-interface DisposeAsync call over an interface call --- standard/statements.md | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/standard/statements.md b/standard/statements.md index 65155e519..86ff6dd4d 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -2024,9 +2024,9 @@ non_ref_local_variable_declaration ; ``` -A ***resource type*** is either a class or non-ref struct that implements either or both of the `System.IDisposable` or `System.IAsyncDisposable` interfaces, which includes a single parameterless method named `Dispose` and/or `DisposeAsync`; or a ref struct that includes a method named `Dispose` having the same signature as that declared by `System.IDisposable`. Code that is using a resource can call `Dispose` or `DisposeAsync` to indicate that the resource is no longer needed. +A ***resource type*** is either a class or non-ref struct that implements the `System.IDisposable` interface, which includes a single parameterless method named `Dispose`, or a ref struct that includes a method named `Dispose` having the same signature as that declared by `System.IDisposable`, or a class or non-ref struct that has an accessible instance `DisposeAsync()` method with an awaitable return type ([§12.9.9.2](expressions.md#12992-awaitable-expressions)), or a class or non-ref struct that implements the `System.IAsyncDisposable` interface, which includes a single parameterless method named `DisposeAsync`. Code that is using a resource can call `Dispose` or `DisposeAsync` to indicate that the resource is no longer needed. -If the form of *resource_acquisition* is *non_ref_local_variable_declaration* then the type of the *non_ref_local_variable_declaration* shall be either `dynamic` or a resource type. If the form of *resource_acquisition* is *expression* then this expression shall have a resource type. If `await` is present, the resource type shall implement `System.IAsyncDisposable`. A `ref struct` type cannot be the resource type for a `using` statement with the `await` modifier. +If the form of *resource_acquisition* is *non_ref_local_variable_declaration* then the type of the *non_ref_local_variable_declaration* shall be either `dynamic` or a resource type. If the form of *resource_acquisition* is *expression* then this expression shall have a resource type. If `await` is present, the resource type shall have an accessible instance `DisposeAsync()` method with an awaitable return type or implement `System.IAsyncDisposable`. A `ref struct` type cannot be the resource type for a `using` statement with the `await` modifier. Local variables declared in a *resource_acquisition* are read-only, and shall include an initializer. A compile-time error occurs if the embedded statement attempts to modify these local variables (via assignment or the `++` and `--` operators), take the address of them, or pass them as reference or output parameters. @@ -2167,19 +2167,42 @@ using (ResourceType rN = eN) > > *end example* -When `ResourceType` is a reference type that implements `IAsyncDisposable`. Other formulations for `await using` perform similar substitutions from the synchronous `Dispose` method to the asynchronous `DisposeAsync` method. An `await using` statement of the form +An `await using` statement of the form ```csharp await using (ResourceType resource = «expression») «statement» ``` -is semantically equivalent to the formulations shown below with `IAsyncDisposable` instead of `IDisposable`, `DisposeAsync` instead of `Dispose`, and the `ValueTask` returned from `DisposeAsync` is `await`ed: +first performs member lookup ([§12.5](expressions.md#125-member-lookup)) on `ResourceType` with the identifier `DisposeAsync` and no type arguments. If the result is a method group and overload resolution ([§12.6.4](expressions.md#1264-overload-resolution)) with an empty argument list selects an accessible instance method, that method is selected for asynchronous disposal. If its return type is not awaitable ([§12.9.9.2](expressions.md#12992-awaitable-expressions)), an error is produced and no further steps are taken. + +When such a method is selected, the statement is semantically equivalent to: ```csharp -await using (ResourceType resource = «expression») «statement» +{ + ResourceType resource = «expression»; + try + { + «statement»; + } + finally + { + if ((object)resource != null) + { + await resource.DisposeAsync(); + } + } +} ``` -is semantically equivalent to: +> *Note*: If `ResourceType` is a nullable value type ([§8.3.12](types.md#8312-nullable-value-types)), member lookup for `DisposeAsync` is performed on `ResourceType`, not on its underlying type. *end note* + + + +> *Note*: When `ResourceType` is a non-nullable value type, the null check shown above is elided. *end note* + +If no such method is selected, the corresponding synchronous formulations apply with `IAsyncDisposable` instead of `IDisposable`, `DisposeAsync` instead of `Dispose`, and the `ValueTask` returned from `DisposeAsync` awaited. The formulation for ref struct resources does not apply, since a ref struct cannot be the resource type of an `await using` statement. + +For example, when `ResourceType` is a reference type that implements `IAsyncDisposable`, the statement is semantically equivalent to: ```csharp { @@ -2199,6 +2222,8 @@ is semantically equivalent to: } ``` +The expression form of `await using` has the same possible formulations, with `resource` being a temporary variable inaccessible to user code. + > *Note*: Any jump statements ([§13.10](statements.md#1310-jump-statements)) in the *embedded_statement* must conform to expanded form of the `using` statement. *end note* ### 13.14.2 Using declaration From 66cca43cbf4f6c3a13411727abe4cad7e979ac51 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Mon, 21 Sep 2026 10:51:58 -0400 Subject: [PATCH 11/16] Null check elision is not required for correctness --- standard/statements.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/standard/statements.md b/standard/statements.md index 86ff6dd4d..c968da48a 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -1424,7 +1424,7 @@ The body of the `finally` block is constructed according to the following steps: finally {} ``` -> *Note*: When `E` is a non-nullable value type, the null checks shown above are elided. *end note* +> *Note*: When `E` is a non-nullable value type, the null checks shown above may be elided. *end note* @@ -2198,7 +2198,7 @@ When such a method is selected, the statement is semantically equivalent to: -> *Note*: When `ResourceType` is a non-nullable value type, the null check shown above is elided. *end note* +> *Note*: When `ResourceType` is a non-nullable value type, the null check shown above may be elided. *end note* If no such method is selected, the corresponding synchronous formulations apply with `IAsyncDisposable` instead of `IDisposable`, `DisposeAsync` instead of `Dispose`, and the `ValueTask` returned from `DisposeAsync` awaited. The formulation for ref struct resources does not apply, since a ref struct cannot be the resource type of an `await using` statement. From 64bd04ba1749f3dac130b8256971175171d89b04 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 30 Sep 2026 20:13:47 -0400 Subject: [PATCH 12/16] Add bullets to long list --- standard/statements.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/standard/statements.md b/standard/statements.md index c968da48a..397e4ce2f 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -2024,7 +2024,14 @@ non_ref_local_variable_declaration ; ``` -A ***resource type*** is either a class or non-ref struct that implements the `System.IDisposable` interface, which includes a single parameterless method named `Dispose`, or a ref struct that includes a method named `Dispose` having the same signature as that declared by `System.IDisposable`, or a class or non-ref struct that has an accessible instance `DisposeAsync()` method with an awaitable return type ([§12.9.9.2](expressions.md#12992-awaitable-expressions)), or a class or non-ref struct that implements the `System.IAsyncDisposable` interface, which includes a single parameterless method named `DisposeAsync`. Code that is using a resource can call `Dispose` or `DisposeAsync` to indicate that the resource is no longer needed. +A ***resource type*** is one of the following: + +- A class or non-ref struct that implements the `System.IDisposable` interface, which includes a single parameterless method named `Dispose`. +- A ref struct that includes a method named `Dispose` having the same signature as that declared by `System.IDisposable`. +- A class or non-ref struct that has an accessible instance `DisposeAsync()` method with an awaitable return type ([§12.9.9.2](expressions.md#12992-awaitable-expressions)). +- A class or non-ref struct that implements the `System.IAsyncDisposable` interface, which includes a single parameterless method named `DisposeAsync`. + +Code that is using a resource can call `Dispose` or `DisposeAsync` to indicate that the resource is no longer needed. If the form of *resource_acquisition* is *non_ref_local_variable_declaration* then the type of the *non_ref_local_variable_declaration* shall be either `dynamic` or a resource type. If the form of *resource_acquisition* is *expression* then this expression shall have a resource type. If `await` is present, the resource type shall have an accessible instance `DisposeAsync()` method with an awaitable return type or implement `System.IAsyncDisposable`. A `ref struct` type cannot be the resource type for a `using` statement with the `await` modifier. From 5f1c31c6e1e84b5894f24ed603d29ac25fcbf7c4 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 30 Sep 2026 21:37:56 -0400 Subject: [PATCH 13/16] Add missed comment about not boxing and update the lowering to more closely resemble the way that this is possible --- standard/statements.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/standard/statements.md b/standard/statements.md index 397e4ce2f..2a29e0ab4 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -2094,15 +2094,16 @@ Otherwise, the formulation is } finally { - IDisposable d = (IDisposable)resource; - if (d != null) + if ((object)resource != null) { - d.Dispose(); + ((IDisposable)resource).Dispose(); } } } ``` +except that the cast of `resource` to `System.IDisposable` shall not cause boxing to occur. + For ref struct resources, the only semantically equivalent formulation is ```csharp @@ -2220,10 +2221,9 @@ For example, when `ResourceType` is a reference type that implements `IAsyncDisp } finally { - IAsyncDisposable d = (IAsyncDisposable)resource; - if (d != null) + if ((object)resource != null) { - await d.DisposeAsync(); + await ((IAsyncDisposable)resource).DisposeAsync(); } } } From 0e1abef5011afef0d8747de4891d2da4b737408f Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 30 Sep 2026 21:44:37 -0400 Subject: [PATCH 14/16] Deduplicate --- standard/statements.md | 48 ++++++++---------------------------------- 1 file changed, 9 insertions(+), 39 deletions(-) diff --git a/standard/statements.md b/standard/statements.md index 2a29e0ab4..00eb3feda 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -1386,48 +1386,14 @@ is semantically equivalent to: In the case where the expression `enumerable` represents a method call expression and one of the parameters is marked with the `EnumeratorCancellationAttribute` ([§23.5.8](attributes.md#2358-the-enumeratorcancellation-attribute)) the `CancellationToken` is passed to the `GetAsyncEnumerator` method. Other library methods may require a `CancellationToken` is passed to `GetAsyncEnumerator`. When those methods are part of the expression `enumerable`, the tokens shall be combined into a single token as if by `CreateLinkedTokenSource` and its `Token` property. -The body of the `finally` block is constructed according to the following steps: - -- Perform member lookup ([§12.5](expressions.md#125-member-lookup)) on `E` with the identifier `DisposeAsync` and no type arguments. If the result is a method group and overload resolution ([§12.6.4](expressions.md#1264-overload-resolution)) with an empty argument list selects an accessible instance method, that method is selected for asynchronous disposal: - - If the return type is not awaitable ([§12.9.9.2](expressions.md#12992-awaitable-expressions)), an error is produced and no further steps are taken. - - Otherwise the `finally` clause is expanded to the semantic equivalent of: - - ```csharp - finally - { - if ((object)e != null) - { - await e.DisposeAsync(); - } - } - ``` - - > *Note*: If `E` is a nullable value type ([§8.3.12](types.md#8312-nullable-value-types)), member lookup for `DisposeAsync` is performed on `E`, not on its underlying type. *end note* - -- Otherwise, if there is an implicit conversion from `E` to the `System.IAsyncDisposable` interface, the `finally` clause is expanded to the semantic equivalent of: - - ```csharp - finally - { - if ((object)e != null) - { - await ((System.IAsyncDisposable)e).DisposeAsync(); - } - } - ``` - - If `E` is a value type, or a type parameter instantiated to a value type, then the conversion of `e` to `System.IAsyncDisposable` shall not cause boxing to occur. +Perform the member lookup and overload resolution for `DisposeAsync` specified for an `await using` statement ([§13.14.1](statements.md#13141-general)), using the enumerator type `E` as `ResourceType`. If an accessible instance method is selected, or if there is an implicit conversion from `E` to `System.IAsyncDisposable`, the `finally` block is constructed as specified there, with `enumerator` in place of `resource`. This includes the error if a selected method has a non-awaitable return type. -- Otherwise, the `finally` clause is expanded to an empty block: - - ```csharp - finally {} - ``` +Otherwise, the `finally` clause is expanded to an empty block: -> *Note*: When `E` is a non-nullable value type, the null checks shown above may be elided. *end note* - +```csharp +finally {} +``` - > *Note*: An `await foreach` is not required to dispose of `e` synchronously if an asynchronous dispose mechanism is not available. *end note* #### 13.9.5.4 Deconstructing foreach @@ -2210,6 +2176,8 @@ When such a method is selected, the statement is semantically equivalent to: If no such method is selected, the corresponding synchronous formulations apply with `IAsyncDisposable` instead of `IDisposable`, `DisposeAsync` instead of `Dispose`, and the `ValueTask` returned from `DisposeAsync` awaited. The formulation for ref struct resources does not apply, since a ref struct cannot be the resource type of an `await using` statement. +If no method is selected and there is no implicit conversion to `System.IAsyncDisposable`, a compile-time error occurs. + For example, when `ResourceType` is a reference type that implements `IAsyncDisposable`, the statement is semantically equivalent to: ```csharp @@ -2229,6 +2197,8 @@ For example, when `ResourceType` is a reference type that implements `IAsyncDisp } ``` +These disposal rules are also used by asynchronous foreach ([§13.9.5.3](statements.md#13953-asynchronous-foreach)). + The expression form of `await using` has the same possible formulations, with `resource` being a temporary variable inaccessible to user code. > *Note*: Any jump statements ([§13.10](statements.md#1310-jump-statements)) in the *embedded_statement* must conform to expanded form of the `using` statement. *end note* From fbfad33d514fec6ced13f4d947be8d6f5f7d7241 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 30 Sep 2026 22:23:06 -0400 Subject: [PATCH 15/16] Make null check elision statement more comprehensive --- standard/statements.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/standard/statements.md b/standard/statements.md index 00eb3feda..3245e6ed4 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -2070,6 +2070,8 @@ Otherwise, the formulation is except that the cast of `resource` to `System.IDisposable` shall not cause boxing to occur. +> *Note*: When `ResourceType` is a non-nullable value type, or a type parameter instantiated to a non-nullable value type, the null check shown above may be elided. *end note* + For ref struct resources, the only semantically equivalent formulation is ```csharp @@ -2172,7 +2174,7 @@ When such a method is selected, the statement is semantically equivalent to: -> *Note*: When `ResourceType` is a non-nullable value type, the null check shown above may be elided. *end note* +> *Note*: When `ResourceType` is a non-nullable value type, or a type parameter instantiated to a non-nullable value type, the null check shown above may be elided. *end note* If no such method is selected, the corresponding synchronous formulations apply with `IAsyncDisposable` instead of `IDisposable`, `DisposeAsync` instead of `Dispose`, and the `ValueTask` returned from `DisposeAsync` awaited. The formulation for ref struct resources does not apply, since a ref struct cannot be the resource type of an `await using` statement. From 2c9df3201b626a32adf11a6f2f6e5ff383c1696e Mon Sep 17 00:00:00 2001 From: jnm2 Date: Thu, 1 Oct 2026 08:14:30 -0400 Subject: [PATCH 16/16] Point await foreach to await using as a lowering --- standard/statements.md | 67 ++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/standard/statements.md b/standard/statements.md index 3245e6ed4..52e9d58a8 100644 --- a/standard/statements.md +++ b/standard/statements.md @@ -1364,37 +1364,42 @@ An `await foreach` statement of the form await foreach (T item in enumerable) «embedded_statement» ``` -is semantically equivalent to: +uses an `await using` statement ([§13.14.1](statements.md#13141-general)) in its expansion if either of the following conditions holds: + +- The member lookup and overload resolution for `DisposeAsync` specified for an `await using` statement, using the enumerator type `E` as `ResourceType`, select an accessible instance method. +- There is an implicit conversion from `E` to `System.IAsyncDisposable`. + +In this case, the statement is semantically equivalent to: ```csharp +await using (E enumerator = enumerable.GetAsyncEnumerator()) { - var enumerator = enumerable.GetAsyncEnumerator(); - try - { - while (await enumerator.MoveNextAsync()) - { - T item = enumerator.Current; - «embedded_statement» - } - } - finally + while (await enumerator.MoveNextAsync()) { - // dispose of enumerator as described later in this clause. + T item = enumerator.Current; + «embedded_statement» } } ``` -In the case where the expression `enumerable` represents a method call expression and one of the parameters is marked with the `EnumeratorCancellationAttribute` ([§23.5.8](attributes.md#2358-the-enumeratorcancellation-attribute)) the `CancellationToken` is passed to the `GetAsyncEnumerator` method. Other library methods may require a `CancellationToken` is passed to `GetAsyncEnumerator`. When those methods are part of the expression `enumerable`, the tokens shall be combined into a single token as if by `CreateLinkedTokenSource` and its `Token` property. - -Perform the member lookup and overload resolution for `DisposeAsync` specified for an `await using` statement ([§13.14.1](statements.md#13141-general)), using the enumerator type `E` as `ResourceType`. If an accessible instance method is selected, or if there is an implicit conversion from `E` to `System.IAsyncDisposable`, the `finally` block is constructed as specified there, with `enumerator` in place of `resource`. This includes the error if a selected method has a non-awaitable return type. +> *Note*: The return type of the selected method is not required to be awaitable in order to choose this expansion. If it is not awaitable, the `await using` statement produces a compile-time error, even if an implicit conversion to `System.IAsyncDisposable` exists. *end note* -Otherwise, the `finally` clause is expanded to an empty block: +If neither condition holds, the statement is semantically equivalent to: ```csharp -finally {} +{ + E enumerator = enumerable.GetAsyncEnumerator(); + while (await enumerator.MoveNextAsync()) + { + T item = enumerator.Current; + «embedded_statement» + } +} ``` -> *Note*: An `await foreach` is not required to dispose of `e` synchronously if an asynchronous dispose mechanism is not available. *end note* +In the case where the expression `enumerable` represents a method call expression and one of the parameters is marked with the `EnumeratorCancellationAttribute` ([§23.5.8](attributes.md#2358-the-enumeratorcancellation-attribute)) the `CancellationToken` is passed to the `GetAsyncEnumerator` method. Other library methods may require a `CancellationToken` is passed to `GetAsyncEnumerator`. When those methods are part of the expression `enumerable`, the tokens shall be combined into a single token as if by `CreateLinkedTokenSource` and its `Token` property. + +> *Note*: An `await foreach` is not required to dispose of `enumerator` synchronously if an asynchronous dispose mechanism is not available. *end note* #### 13.9.5.4 Deconstructing foreach @@ -1443,31 +1448,11 @@ An `await foreach` statement of the form: await foreach («deconstructor» in enumerable) «embedded_statement» ``` -is semantically equivalent to: - -```csharp -{ - var enumerator = enumerable.GetAsyncEnumerator(); - try - { - while (await enumerator.MoveNextAsync()) - { - «deconstructor» = enumerator.Current; - «embedded_statement» - } - } - finally - { - // dispose of enumerator as for asynchronous foreach - } -} -``` - -This follows the behavior of asynchronous foreach ([§13.9.5.3](statements.md#13953-asynchronous-foreach)), differing by replacing the declaration and initialisation of a single iteration variable with a *deconstructing_assignment* in which the *deconstructor* contains the iteration-variable declarations: each *declaration_expression* other than a discard declares one iteration variable, and each discard ([§9.2.9.2](variables.md#9292-discards)) declares none: +uses the same expansions as asynchronous foreach ([§13.9.5.3](statements.md#13953-asynchronous-foreach)), replacing `T item = enumerator.Current;` with `«deconstructor» = enumerator.Current;`. This *deconstructing_assignment* contains the iteration-variable declarations in its *deconstructor*: each *declaration_expression* other than a discard declares one iteration variable, and each discard ([§9.2.9.2](variables.md#9292-discards)) declares none: -- `enumerator` is not visible or accessible anywhere in the program except as indicated in the above code +- `enumerator` is not visible or accessible anywhere in the program except as indicated in those expansions - the variables declared within the «deconstructor» are read-only to the «embedded_statement» -- the code in the `finally` block is determined as for asynchronous foreach +- the conditions for using the `await using` expansion are the same as for asynchronous foreach > *Example*: A deconstructing foreach uses discards as placeholders for elements that are not needed. Here each element of the collection is a tuple whose second element is discarded: >