Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C# 7.x §15.15.1 task type must not have multiple type arguments #856

Closed
KalleOlaviNiemitalo opened this issue Jul 14, 2023 · 4 comments
Closed
Milestone

Comments

@KalleOlaviNiemitalo
Copy link
Contributor

Describe the bug

In the C# 7.x draft, §15.15.1 (Async functions / General) restricts the genericity of a task type:

For an async method that returns a value, a task type shall be generic. For an async method that does not return a value, a task type shall not be generic.

This allows a constructed task type with more than one type argument, but Roslyn does not support that.

Example

The standard should not allow:

using System;
using System.Runtime.CompilerServices;

class C {
    // «TaskType»<T> is CustomTask<int, int>.
    // «TaskBuilderType»<T> is not clear, but not outright disallowed either.
    async CustomTask<int, int> M() => 1; // error CS1983
}

[AsyncMethodBuilder(typeof(CustomTaskBuilder<>))]
public struct CustomTask<T1, T2> {}

public struct CustomTaskBuilder<TResult> {
    public static CustomTaskBuilder<TResult> Create() => default;
    public void Start<TStateMachine>(
        ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine {}
    public void SetStateMachine(
        IAsyncStateMachine stateMachine) {}
    public void SetException(
        Exception exception) {}
    public void SetResult(TResult result) {}
    public void AwaitOnCompleted<TAwaiter,TStateMachine>(
        ref TAwaiter awaiter,
        ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine {}
    public void AwaitUnsafeOnCompleted<TAwaiter,TStateMachine>(
        ref TAwaiter awaiter,
        ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine {}
    public CustomTask<TResult, TResult> Task => default;
}

Expected behavior

Declare that the task type shall not have more than one type argument.

(Use "type argument" rather than "type parameter", because task types are constructed types.)

Additional context

Unlike a task builder type, a task type can be nested in a generic type. Example:

using System;
using System.Runtime.CompilerServices;

class C {
    // «TaskType»<T> is Outer<char>.CustomTask<int>.
    // «TaskBuilderType»<T> is CustomTaskBuilder<int>; the type argument in Outer<char> does not affect this.
    async Outer<char>.CustomTask<int> M() => 1;
}

public class Outer<T1> {
    [AsyncMethodBuilder(typeof(CustomTaskBuilder<>))]
    public struct CustomTask<T2> {}
}

public struct CustomTaskBuilder<TResult> {
    public static CustomTaskBuilder<TResult> Create() => default;
    public void Start<TStateMachine>(
        ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine {}
    public void SetStateMachine(
        IAsyncStateMachine stateMachine) {}
    public void SetException(
        Exception exception) {}
    public void SetResult(TResult result) {}
    public void AwaitOnCompleted<TAwaiter,TStateMachine>(
        ref TAwaiter awaiter,
        ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine {}
    public void AwaitUnsafeOnCompleted<TAwaiter,TStateMachine>(
        ref TAwaiter awaiter,
        ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine {}
    public Outer<char>.CustomTask<TResult> Task => default;
}
@KalleOlaviNiemitalo
Copy link
Contributor Author

Fixing this is complicated by the following paragraph in §15.2.1:

A class declaration that supplies a type_parameter_list is a generic class declaration. Additionally, any class nested inside a generic class declaration or a generic struct declaration is itself a generic class declaration, since type arguments for the containing type shall be supplied to create a constructed type (§8.4).

interacting with §15.15.1

For an async method that does not return a value, a task type shall not be generic.

when the task type is nested in a generic type. Perhaps then, §15.15.1 should talk about the presence or absence of a type_parameter_list, rather than the type being generic or not.

jskeet added a commit to jskeet/csharpstandard that referenced this issue Aug 7, 2023
- `Task` and `Task<T>` are classified as task types despite not
  specifying builder types
- Interfaces are allowed to be task types
- Enums and methods are prohibited from being decorated with
  AsyncMethodBuilderAttribute
- Task types must not be generic in more than one type parameter
  (including in terms of containing types)

Discussion required for all of this, but if merged, would
fix dotnet#854, dotnet#856, dotnet#858 and dotnet#859.
@jskeet jskeet added this to the C# 7.x milestone Aug 7, 2023
@KalleOlaviNiemitalo
Copy link
Contributor Author

Got a funny idea for how to define a task builder for a task type nested in a generic type, without hardcoding the type arguments of the containing type. Alas, Roslyn rejects it, and the current C# 7.x wording doesn't allow it either.

using System;
using System.Runtime.CompilerServices;

class C {
    // «TaskType»<T> is Outer<char>.CustomTask<int>.
    // «TaskBuilderType»<T> is CustomTaskBuilder<int>; the type argument in Outer<char> does not affect this.
    async Outer<char>.CustomTask<int> M() => 1; // error CS8204
}

public class Outer<T1> {
    [AsyncMethodBuilder(typeof(CustomTaskBuilder<>))]
    public struct CustomTask<T2>
    {
        public static implicit operator CustomTask<T2>(
            CustomTaskBuilder<T2>.ConvertibleTask convertibleTask)
        => default;
    }
}

public struct CustomTaskBuilder<TResult> {
    public static CustomTaskBuilder<TResult> Create() => default;
    public void Start<TStateMachine>(
        ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine {}
    public void SetStateMachine(
        IAsyncStateMachine stateMachine) {}
    public void SetException(
        Exception exception) {}
    public void SetResult(TResult result) {}
    public void AwaitOnCompleted<TAwaiter,TStateMachine>(
        ref TAwaiter awaiter,
        ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine {}
    public void AwaitUnsafeOnCompleted<TAwaiter,TStateMachine>(
        ref TAwaiter awaiter,
        ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine {}

    // This is not exactly «TaskType»<T>, but has an implicit conversion to that.
    public ConvertibleTask Task => default;
    
    public struct ConvertibleTask {}
}

@jskeet
Copy link
Contributor

jskeet commented Aug 8, 2023

Thinking about this in the context of #875, I think I'll make a change to prohibit generic containing types entirely. It makes everything much simpler... I think we can leave any laxness on Roslyn's side as an extension for now.

BillWagner pushed a commit that referenced this issue Aug 15, 2023
* Clarify task types

- `Task` and `Task<T>` are classified as task types despite not
  specifying builder types
- Interfaces are allowed to be task types
- Enums and methods are prohibited from being decorated with
  AsyncMethodBuilderAttribute
- Task types must not be generic in more than one type parameter
  (including in terms of containing types)

Discussion required for all of this, but if merged, would
fix #854, #856, #858 and #859.

* Prevent nesting within generic types, for task types.

This wording follows the wording in 15.15.2 for task builders.
@jskeet
Copy link
Contributor

jskeet commented Aug 16, 2023

Closed by #875.

@jskeet jskeet closed this as completed Aug 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants