* Report an error for address-of in iterators

* Specify unsafe iterator methods

* Note possibility to allow address-of of non-hoisted vars

* Clarify

* Fix formatting

* Make the whole iterator declaration safe

* Expand on unsafe-inheriting iterators alternative

* Clarify

* Clarify

* Use "block-based" + "unsafe signatures only"

* Note that unsafe iterators could be an error

* Fix example

* Clarify
This commit is contained in:
Jan Jones 2024-05-02 10:16:39 +02:00 коммит произвёл GitHub
Родитель df531094a6
Коммит 88342d6c32
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
1 изменённых файлов: 179 добавлений и 3 удалений

Просмотреть файл

@ -43,11 +43,130 @@ lock (this)
The following changes are tied to LangVersion, i.e., C# 12 and lower will continue to disallow
ref-like locals and `unsafe` blocks in async methods and iterators,
and C# 13 will lift these restrictions as described below.
However, spec clarifications which match the existing Roslyn implementation should hold across all LangVersions.
[§13.3.1 Blocks > General][blocks-general]:
> ~~It is a compile-time error for an iterator block to contain an unsafe context ([§23.2][unsafe-contexts]).~~
> An iterator block always defines a safe context, even when its declaration is nested in an unsafe context.
> A *block* that contains one or more `yield` statements ([§13.15][yield-statement]) is called an iterator block,
> **even if those `yield` statements are contained only indirectly in nested blocks (excluding nested lambdas and local functions).**
>
> [...]
>
> ~~It is a compile-time error for an iterator block to contain an unsafe context ([§23.2][unsafe-contexts]).
> An iterator block always defines a safe context, even when its declaration is nested in an unsafe context.~~
> **The iterator block used to implement an iterator ([§15.14][iterators])
> always defines a safe context, even when the iterator declaration is nested in an unsafe context.**
From this spec it also follows:
- If an iterator declaration is marked with the `unsafe` modifier, the signature is in an unsafe scope
but the iterator block used to implement that iterator still defines a safe scope.
- The `set` accessor of an iterator property or indexer (i.e., its `get` accessor is implemented via an iterator block)
"inherits" its safe/unsafe scope from the declaration.
- This does not affect partial declarations without implementation as they are only signatures and cannot have an iterator body.
Note that in C# 12 it is an error to have an iterator method marked with the `unsafe` modifier,
but that is allowed in C# 13 due to the spec change.
For example:
```cs
using System.Collections.Generic;
using System.Threading.Tasks;
class A : System.Attribute { }
unsafe partial class C1
{ // unsafe context
[/* unsafe context */ A]
IEnumerable<int> M1(
/* unsafe context */ int*[] x)
{ // safe context (this is the iterator block implementing the iterator)
yield return 1;
}
IEnumerable<int> M2()
{ // safe context (this is the iterator block implementing the iterator)
unsafe
{ // unsafe context
{ // unsafe context (this is *not* the block implementing the iterator)
yield return 1; // error: `yield return` in unsafe context
}
}
}
[/* unsafe context */ A]
unsafe IEnumerable<int> M3(
/* unsafe context */ int*[] x)
{ // safe context
yield return 1;
}
[/* unsafe context */ A]
IEnumerable<int> this[
/* unsafe context */ int*[] x]
{ // unsafe context
get
{ // safe context
yield return 1;
}
set { /* unsafe context */ }
}
[/* unsafe context */ A]
unsafe IEnumerable<int> this[
/* unsafe context */ long*[] x]
{ // unsafe context (the iterator declaration is unsafe)
get
{ // safe context
yield return 1;
}
set { /* unsafe context */ }
}
IEnumerable<int> M4()
{
yield return 1;
var lam1 = async () =>
{ // safe context
// spec violation: in Roslyn, this is an unsafe context in LangVersion 12 and lower
await Task.Yield();
};
unsafe
{
var lam2 = () =>
{ // unsafe context, lambda cannot be an iterator
yield return 1; // error: yield cannot be used in lambda
};
}
async void local()
{ // safe context
// spec violation: in Roslyn, this is an unsafe context in LangVersion 12 and lower
await Task.Yield();
}
local();
}
public partial IEnumerable<int> M5() // unsafe context (inherits from parent)
{ // safe context
yield return 1;
}
}
partial class C1
{
public partial IEnumerable<int> M5(); // safe context (inherits from parent)
}
class C2
{ // safe context
[/* unsafe context */ A]
unsafe IEnumerable<int> M(
/* unsafe context */ int*[] x)
{ // safe context
yield return 1;
}
unsafe IEnumerable<int> this[
/* unsafe context */ int*[] x]
{ // unsafe context
get
{ // safe context
yield return 1;
}
set { /* unsafe context */ }
}
}
```
[§13.6.2.4 Ref local variable declarations][ref-local]:
@ -74,6 +193,12 @@ hence this error falls outside the boundaries of what we want to downgrade to wa
Note that [the new `Lock`-object-based `lock`][lock-object] reports compile-time errors for `yield return`s in its body,
because such `lock` statement is equivalent to a `using` on a `ref struct` which disallows `yield return`s in its body.
[§15.14.1 Iterators > General][iterators]:
> When a function member is implemented using an iterator block,
> it is a compile-time error for the formal parameter list of the function member to specify any
> `in`, `ref readonly`, `out`, or `ref` parameters, or an parameter of a `ref struct` type **or a pointer type**.
No change in the spec is needed to allow `unsafe` blocks which do not contain `await`s in async methods,
because the spec has never disallowed `unsafe` blocks in async methods.
However, the spec should have always disallowed `await` inside `unsafe` blocks
@ -88,6 +213,14 @@ so we propose the following change to the spec:
> **It is a compile-time error for an unsafe context ([§23.2][unsafe-contexts]) to contain
> an `await` expression ([§12.9.8][await-expressions]) or a `yield return` statement ([§13.15][yield-statement]).**
[§23.6.5 The address-of operator][address-of]:
> **A compile-time error will be reported for taking an address of a local or a parameter in an iterator.**
Currently, taking an address of a local or a parameter in an async method is [a warning in C# 12 warning wave][async-pointer].
---
Note that more constructs can work thanks to `ref` allowed inside segments without `await` and `yield` in async/iterator methods
even though no spec change is needed specifically for them as it all falls out from the aforementioned spec changes:
@ -155,6 +288,10 @@ class C
> or the variable needs to be hoisted as part of an async ([§15.15][async-funcs]) or an iterator ([§15.14][iterators]) method**.
> - [...]
- Currently, we have an existing warning in C# 12 warning wave for address-of in async methods and a proposed error for address-of in iterators
reported for LangVersion 13+ (does not need to be reported in earlier versions because it was impossible to use unsafe code in iterators).
We could relax both of these to apply only to variables that are actually hoisted, not all locals and parameters.
- It could be possible to use `fixed` to get the address of a hoisted or captured variable
although the fact that those are fields is an implementation detail
so in other implementations it might not be possible to use `fixed` on them.
@ -162,7 +299,10 @@ class C
but captured variables were already "moveable" and `fixed` was not allowed for them.
- We could allow `await`/`yield` inside `unsafe` except inside `fixed` statements (compiler cannot pin variables across method boundaries).
That might result in some unexpected behavior, for example around `stackalloc` as described below.
That might result in some unexpected behavior, for example around `stackalloc` as described in the nested bullet point below.
Otherwise, hoisting pointers is supported even today in some scenarios (there is an example below related to pointers as arguments),
so there should be no other limitations in allowing this.
- We could disallow the unsafe variant of `stackalloc` in async/iterator methods,
because the stack-allocated buffer does not live across `await`/`yield` statements.
It does not feel necessary because unsafe code by design does not prevent "use after free".
@ -173,6 +313,41 @@ class C
(Note that it would not be *impossible* to use the `stackalloc` result across `await`/`yield` similarly as
you can save any `fixed` pointer today into another pointer variable and use it outside the `fixed` block.)
- Iterator and async methods could be allowed to have pointer parameters.
They would need to be hoisted, but that should not be a problem as
hoisting pointers is supported even today, for example:
```cs
unsafe public void* M(void* p)
{
var d = () => p;
return d();
}
```
- The proposal currently keeps (and extends/clarifies) the pre-existing spec that
iterator methods begin a safe context even if they are in an unsafe context.
For example, an iterator method is not an unsafe context even if it is defined in a class which has the `unsafe` modifier.
Alternatively, we could make iterators "inherit" the `unsafe` modifier like other methods do.
- Advantage: removes complexity from the spec and implementation.
- Advantage: aligns iterators with async methods (one of the motivations of the feature).
- Disadvantage: iterators inside unsafe classes could not contain `yield return` statements,
such iterators would have to be defined in a separate partial class declaration without the `unsafe` modifier.
- Disadvantage: this would be a breaking change in LangVersion=13 (iterators in unsafe classes are allowed in C# 12).
- Instead of an iterator defining a safe context for the body only, the whole signature could be a safe context.
That is inconsistent with the rest of the language in that bodies normally do not affect declarations but here
a declaration would be either safe or unsafe depending on whether the body is an iterator or not.
It would be also a breaking change in LangVersion=13 as in C# 12 iterator signatures are unsafe (they can contain pointer array parameters, for example).
- Applying the `unsafe` modifier to an iterator:
- Could affect the body as well as the signature. Such iterators would not be very useful though
because their unsafe bodies could not contain `yield return`s, they could have only `yield break`s.
- Could be an error in `LangVersion >= 13` as it is in `LangVersion <= 12` because
it is not very useful to have an unsafe iterator member as it only allows one to have
pointer array parameters or unsafe setters without additional unsafe block.
But normal pointer arguments could be allowed in the future.
[definite-assignment]: https://github.com/dotnet/csharpstandard/blob/ee38c3fa94375cdac119c9462b604d3a02a5fcd2/standard/variables.md#94-definite-assignment
[simple-names]: https://github.com/dotnet/csharpstandard/blob/ee38c3fa94375cdac119c9462b604d3a02a5fcd2/standard/expressions.md#1284-simple-names
[await-expressions]: https://github.com/dotnet/csharpstandard/blob/ee38c3fa94375cdac119c9462b604d3a02a5fcd2/standard/expressions.md#1298-await-expressions
@ -188,6 +363,7 @@ class C
[async-funcs-general]: https://github.com/dotnet/csharpstandard/blob/ee38c3fa94375cdac119c9462b604d3a02a5fcd2/standard/classes.md#15151-general
[unsafe-contexts]: https://github.com/dotnet/csharpstandard/blob/ee38c3fa94375cdac119c9462b604d3a02a5fcd2/standard/unsafe-code.md#232-unsafe-contexts
[fixed-vars]: https://github.com/dotnet/csharpstandard/blob/ee38c3fa94375cdac119c9462b604d3a02a5fcd2/standard/unsafe-code.md#234-fixed-and-moveable-variables
[address-of]: https://github.com/dotnet/csharpstandard/blob/ee38c3fa94375cdac119c9462b604d3a02a5fcd2/standard/unsafe-code.md#2365-the-address-of-operator
[lock-object]: ./lock-object.md
[ref-safety-unsafe-warnings]: https://github.com/dotnet/csharplang/issues/6476
[async-pointer]: https://github.com/dotnet/roslyn/pull/66915