diff --git a/docs/runtime-async.md b/docs/runtime-async.md new file mode 100644 index 00000000000..6e8be61cbe7 --- /dev/null +++ b/docs/runtime-async.md @@ -0,0 +1,468 @@ +# Runtime Async in F# + +Runtime async is a .NET 10.0+ feature that lets you write async methods without a state machine. Instead of the compiler generating a state machine (as `task { }` and `async { }` do), the runtime itself handles suspension and resumption. The result is flatter IL, lower overhead, and simpler generated code. + +F# exposes this feature in two ways: + +- The `runtimeTask { }` computation expression, for most use cases. +- Direct `[]` annotation, for library authors who need full control. + +--- + +## Prerequisites + +Before using runtime async, you need: + +**Target framework**: `net10.0` or later. + +```xml +net10.0 +``` + +**Language version**: `preview` (the feature is gated behind preview). + +```xml +preview +``` + +Or pass `--langversion:preview` to the compiler directly. + +**Runtime environment variable**: `DOTNET_RuntimeAsync=1` must be set **before** the CLR loads any type that contains runtime-async methods. Setting it inside your program code is too late. + +```bash +# On Linux/macOS +export DOTNET_RuntimeAsync=1 +dotnet run + +# On Windows (Command Prompt) +set DOTNET_RuntimeAsync=1 +dotnet run + +# On Windows (PowerShell) +$env:DOTNET_RuntimeAsync = "1" +dotnet run +``` + +> **Important**: If `DOTNET_RuntimeAsync=1` is not set before the CLR loads the type, the runtime will throw at the call site. This is a runtime check, not a compile-time check. + +--- + +## Using `runtimeTask { }` + +The `runtimeTask` computation expression is the primary way to write runtime-async methods in F#. It produces a `Task<'T>` and emits flat IL with `AsyncHelpers.Await` calls rather than a state machine. + +Add `#nowarn "57"` to suppress the preview feature warning. + +### Basic Usage + +```fsharp +#nowarn "57" +open System.Threading.Tasks +open Microsoft.FSharp.Control + +let greet (name: string) : Task = + runtimeTask { + return $"Hello, {name}!" + } +``` + +### Awaiting Tasks + +You can `let!` bind `Task<'T>`, `Task`, `ValueTask<'T>`, and `ValueTask`: + +```fsharp +#nowarn "57" +open System.Threading.Tasks +open Microsoft.FSharp.Control + +let fetchAndDouble (t: Task) : Task = + runtimeTask { + let! value = t + return value * 2 + } + +let awaitUnitTask (t: Task) : Task = + runtimeTask { + let! () = t + return () + } + +let awaitValueTask (t: ValueTask) : Task = + runtimeTask { + let! value = t + return value + 1 + } +``` + +### Multiple Awaits + +```fsharp +#nowarn "57" +open System.Threading.Tasks +open Microsoft.FSharp.Control + +let addResults (a: Task) (b: Task) : Task = + runtimeTask { + let! x = a + let! y = b + return x + y + } +``` + +### Control Flow + +`while` loops and `for` loops over sequences work as expected: + +```fsharp +#nowarn "57" +open System.Threading.Tasks +open Microsoft.FSharp.Control + +let processItems (items: seq>) : Task = + runtimeTask { + let mutable total = 0 + for item in items do + let! value = item + total <- total + value + return total + } + +let countdown (start: int) : Task = + runtimeTask { + let mutable i = start + while i > 0 do + printfn "%d" i + i <- i - 1 + } +``` + +### Error Handling + +`try/with` and `try/finally` both work inside `runtimeTask { }`: + +```fsharp +#nowarn "57" +open System.Threading.Tasks +open Microsoft.FSharp.Control + +let safeAwait (t: Task) : Task = + runtimeTask { + try + let! value = t + return value + with ex -> + printfn "Error: %s" ex.Message + return -1 + } + +let withCleanup (t: Task) : Task = + runtimeTask { + try + let! value = t + return value + finally + printfn "Cleanup complete" + } +``` + +### Disposing Resources + +Use `use` (not `use!`) to dispose `IDisposable` resources: + +```fsharp +#nowarn "57" +open System.IO +open System.Threading.Tasks +open Microsoft.FSharp.Control + +let readFile (path: string) : Task = + runtimeTask { + use reader = new StreamReader(path) + let! line = Task.Run(fun () -> reader.ReadLine()) + return line + } +``` + +--- + +## Limitations Compared to `task { }` + +`runtimeTask { }` is intentionally minimal. It covers the common cases but does not replicate every feature of `task { }`. + +| Feature | `task { }` | `runtimeTask { }` | +|---|---|---| +| `let! x = someTask` | Yes | Yes | +| `return x` | Yes | Yes | +| `return!` | Yes | **No** | +| `and!` (parallel bind) | Yes | **No** | +| `use!` (async dispose) | Yes | **No** (only `use` for `IDisposable`) | +| `do! Task.Yield()` | Yes | **No** | +| `let! x = async { ... }` | Yes | **No** | +| `IAsyncDisposable` in `use` | Yes | **No** | +| Returns `Task<'T>` | Yes | Yes | +| Returns `Task` | Yes | **No** | +| Returns `ValueTask<'T>` | Yes | **No** | +| Background variant | `backgroundTask { }` | **No** | + +If you need any of the unsupported features, use `task { }` instead. You can freely interop between the two: a `runtimeTask { }` method can `let!` the result of a `task { }` method, and vice versa. + +```fsharp +#nowarn "57" +open System.Threading.Tasks +open Microsoft.FSharp.Control + +// task CE produces a Task +let computeWithStateMachine () : Task = + task { return 42 } + +// runtimeTask CE awaits it without a state machine +let consumeWithRuntimeAsync () : Task = + runtimeTask { + let! result = computeWithStateMachine() + return result * 2 + } +``` + +--- + +## Direct Usage with `[]` + +For library authors or cases where you need more control, you can annotate a method directly with `[]` and call `AsyncHelpers.Await` yourself. + +This approach supports all four task-like return types: `Task`, `Task<'T>`, `ValueTask`, and `ValueTask<'T>`. + +Add `#nowarn "57"` for the preview warning and `#nowarn "SYSLIB5007"` when calling `AsyncHelpers.Await` directly. + +### Supported Return Types + +```fsharp +#nowarn "57" +#nowarn "SYSLIB5007" +open System.Runtime.CompilerServices +open System.Threading.Tasks + +// Returns Task<'T> +[] +let asyncReturnInt () : Task = 42 + +// Returns Task (non-generic) +[] +let asyncReturnUnit () : Task = () + +// Returns ValueTask<'T> +[] +let asyncReturnValueTask () : ValueTask = 42 + +// Returns ValueTask (non-generic) +[] +let asyncReturnValueTaskUnit () : ValueTask = () +``` + +### Awaiting with AsyncHelpers.Await + +```fsharp +#nowarn "57" +#nowarn "SYSLIB5007" +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +let awaitAndDouble (t: Task) : Task = + let value = AsyncHelpers.Await t + value * 2 + +[] +let awaitMultiple (a: Task) (b: Task) : Task = + let x = AsyncHelpers.Await a + let y = AsyncHelpers.Await b + x + y + +// Generic method +[] +let genericAwait<'T> (t: Task<'T>) : Task<'T> = + AsyncHelpers.Await t +``` + +### Error Handling + +```fsharp +#nowarn "57" +#nowarn "SYSLIB5007" +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +let safeAwait (t: Task) : Task = + try + AsyncHelpers.Await t + with _ -> + -1 + +[] +let withFinally (t: Task) : Task = + try + AsyncHelpers.Await t + finally + printfn "done" +``` + +### Interop with `task { }` + +```fsharp +#nowarn "57" +#nowarn "SYSLIB5007" +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +let interopWithTaskCE () : Task = + let t = task { return 42 } + AsyncHelpers.Await t +``` + +--- + +## Compiler Errors + +The compiler enforces several rules when you use `[]`. These errors apply to direct usage; the `runtimeTask { }` CE handles them internally. + +### Error 3884: Invalid return type + +Methods marked with `MethodImplOptions.Async` must return `Task`, `Task<'T>`, `ValueTask`, or `ValueTask<'T>`. + +```fsharp +// Error FS3884: Methods marked with MethodImplOptions.Async must return Task, +// Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is 'int'. +[] +let bad () : int = 42 // wrong return type +``` + +### Error 3885: Conflict with Synchronized + +`MethodImplOptions.Async` cannot be combined with `MethodImplOptions.Synchronized`. + +```fsharp +// Error FS3885: Methods marked with MethodImplOptions.Async cannot also use +// MethodImplOptions.Synchronized. +[] +let bad () : Task = 42 +``` + +### Error 3886: Byref return type + +Methods marked with `MethodImplOptions.Async` cannot return byref types. + +```fsharp +// Error FS3886: Methods marked with MethodImplOptions.Async cannot return byref types. +[] +let bad (x: byref) : Task> = ... +``` + +### Error 3887: Runtime not supported + +If you target a framework older than .NET 10.0, the compiler emits this error. + +``` +Error FS3887: Methods marked with MethodImplOptions.Async are not supported in this context. +``` + +Make sure your project targets `net10.0` or later and uses `--langversion:preview`. + +--- + +## For Library Authors + +### RuntimeAsyncAttribute + +`RuntimeAsyncAttribute` is a marker attribute in `Microsoft.FSharp.Control` for annotating types that contain runtime-async methods. It's intended for future library extensibility. + +```fsharp +open Microsoft.FSharp.Control + +[] +type MyAsyncService() = + member _.DoWork() : Task = + runtimeTask { return 42 } +``` + +**Important**: The compiler does not read `RuntimeAsyncAttribute` to propagate the async IL flag (0x2000). The attribute is a no-op marker today. It exists to signal intent and reserve the design space for future tooling. + +The attribute targets classes only (`AttributeTargets.Class`) and cannot be applied multiple times to the same type. + +### How the Async Flag Propagates + +The 0x2000 IL flag is what tells the runtime to use runtime-async semantics instead of a state machine. Here's how it gets onto your methods: + +1. The `RuntimeTaskBuilder.Run` method has `[ 0x2000)>]` on it. +2. All builder members are `inline`, so the entire `runtimeTask { }` body gets inlined into the call site. +3. The F# compiler's IL generator (`IlxGen.fs`) detects `AsyncHelpers.Await` call sites in the inlined method body via `ExprContainsAsyncHelpersAwaitCall`. +4. When detected, the compiler emits the 0x2000 flag on the containing method. + +For direct `[]` usage, the flag comes from the attribute itself. + +The key insight: **the flag propagates through `AsyncHelpers.Await` detection, not through `RuntimeAsyncAttribute`**. If you write a custom builder or helper that calls `AsyncHelpers.Await`, the compiler will propagate the flag automatically. + +### Writing a Custom Builder + +If you want to write your own computation expression builder that produces runtime-async methods, follow the same pattern as `RuntimeTaskBuilder`: + +1. Make all members `inline` with `[]` on continuation parameters. +2. Put `[ 0x2000)>]` on the `Run` member. +3. Call `AsyncHelpers.Await` in your `Bind` members. + +```fsharp +#nowarn "57" +#nowarn "SYSLIB5007" +open System +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +type MyRuntimeBuilder() = + member inline _.Return(x: 'T) : 'T = x + member inline _.Bind(t: Task<'T>, [] f: 'T -> 'U) : 'U = + f(AsyncHelpers.Await t) + member inline _.Delay([] f: unit -> 'T) : 'T = f() + [ 0x2000)>] + member inline _.Run([] f: unit -> 'T) : Task<'T> = + // Cast is needed because the body returns 'T but Run must return Task<'T>. + // The runtime handles the wrapping when the 0x2000 flag is present. + (# "" (f()) : Task<'T> #) + +let myRuntime = MyRuntimeBuilder() +``` + +> **Note**: The `(# "" ... #)` cast is an internal F# IL trick. In practice, use `RuntimeTaskBuilder` directly rather than reimplementing it. + +--- + +## Quick Reference + +```fsharp +// Project file requirements: +// net10.0 +// preview + +// Environment (before CLR loads the type): +// DOTNET_RuntimeAsync=1 + +#nowarn "57" +open System.Threading.Tasks +open Microsoft.FSharp.Control + +// CE usage (most common) +let example1 (t: Task) : Task = + runtimeTask { + let! x = t + return x + 1 + } + +// Direct attribute usage (library authors) +#nowarn "SYSLIB5007" +open System.Runtime.CompilerServices + +[] +let example2 (t: Task) : Task = + let x = AsyncHelpers.Await t + x + 1 +``` diff --git a/docs/samples/runtime-async-library/ILDasm.targets b/docs/samples/runtime-async-library/ILDasm.targets new file mode 100644 index 00000000000..1af862b9200 --- /dev/null +++ b/docs/samples/runtime-async-library/ILDasm.targets @@ -0,0 +1,24 @@ + + + + + + + + + + <_ILDasmExe Condition="$([MSBuild]::IsOSPlatform('Windows'))">ildasm.exe + <_ILDasmExe Condition="!$([MSBuild]::IsOSPlatform('Windows'))">ildasm + <_ILDasmDir>$(NuGetPackageRoot)runtime.$(NETCoreSdkPortableRuntimeIdentifier).microsoft.netcore.ildasm/10.0.0/runtimes/$(NETCoreSdkPortableRuntimeIdentifier)/native/ + <_ILDasmPath>$(_ILDasmDir)$(_ILDasmExe) + <_ILOutputPath>$(MSBuildProjectDirectory)/$(AssemblyName).il + + + + + + + + + diff --git a/docs/samples/runtime-async-library/README.md b/docs/samples/runtime-async-library/README.md new file mode 100644 index 00000000000..487f35e437a --- /dev/null +++ b/docs/samples/runtime-async-library/README.md @@ -0,0 +1,200 @@ +## Runtime Async CE Library Sample + +This sample demonstrates a `runtimeTask` computation expression (CE) defined in a library project and consumed by a separate app project. The key design insight is that **`Run` is non-inline with `[]`** — the compiler emits it as `cil managed async` directly, and CE body closures are also `cil managed async` (because they contain `AsyncHelpers.Await` calls from inlined `Bind` members). + +It is wired to the repo-built compiler so runtime-async IL is emitted end-to-end. + +### Projects + +- `RuntimeAsync.Library`: defines `RuntimeTaskBuilder` and task-returning library APIs using `runtimeTask`, plus `SimpleAsyncResource` (IAsyncDisposable) and `AsyncRange` (IAsyncEnumerable) helper types +- `RuntimeAsync.Demo`: references the library and runs all 12 example scenarios + +### Key Design + +The working solution uses a **non-inline Run + async closures** pattern: + +#### RuntimeAsync Attribute + +`[]` on the builder class is the single entry point for all runtime-async compiler behavior: + +- It implicitly applies `NoDynamicInvocation` to all public inline members, so no explicit `[]` is needed on `Bind`, `Using`, or `For`. +- It gates the optimizer anti-inlining behavior (Fix 2 below). + +```fsharp +[] +type RuntimeTaskBuilder() = + // Delay wraps the CE body in a closure that is 'cil managed async'. + // The compiler automatically injects the sentinel (AsyncHelpers.Await(ValueTask.CompletedTask)) + // into every Delay closure body, ensuring cloIsAsync = true even with no let!/do! bindings. + // The compiler also handles 'T → Task<'T> bridging automatically for [] builders, + // so no cast helper is needed. + member inline _.Delay([] f: unit -> 'T) : unit -> Task<'T> = + fun () -> f() + + // Run is non-inline with [] — emitted as 'cil managed async'. + // Delay closure returns Task<'T> at runtime (the 'cil managed async' runtime wraps T→Task). + // Run awaits the closure result, then wraps T→Task (because Run itself is 'cil managed async'). + [ 0x2000)>] + member _.Run(f: unit -> Task<'T>) : Task<'T> = + AsyncHelpers.Await(f()) + + // Bind members — NoDynamicInvocation is implicit from [] on the type. + member inline _.Bind(t: Task<'T>, [] f: 'T -> 'U) : 'U = + f(AsyncHelpers.Await t) + // ... overloads for Task, ValueTask<'T>, ValueTask, + // ConfiguredTaskAwaitable, ConfiguredTaskAwaitable<'T>, + // ConfiguredValueTaskAwaitable, ConfiguredValueTaskAwaitable<'T> + + // IAsyncDisposable and IAsyncEnumerable as intrinsic members + // (higher priority than IDisposable/seq extensions) + member inline this.Using(resource: 'T when 'T :> IAsyncDisposable, body: 'T -> 'U) : 'U = ... + member inline _.For(sequence: IAsyncEnumerable<'T>, body: 'T -> unit) : unit = ... + +// Extension (lower priority): generic Bind for any awaitable via SRTP + UnsafeAwaitAwaiter +type RuntimeTaskBuilder with + member inline _.Bind(awaitable: ^Awaitable, f: ^TResult -> 'U) : 'U + when ^Awaitable : (member GetAwaiter: unit -> ^Awaiter) + and ^Awaiter :> ICriticalNotifyCompletion = ... +``` + +Consumer API functions use `runtimeTask { ... }` with **no attribute**: + +```fsharp +// No [] needed here — consumer just calls Run and returns the Task. +let addFromTaskAndValueTask (left: Task) (right: ValueTask) : Task = + runtimeTask { + let! l = left + let! r = right + return l + r + } +``` + +The consumer function calls `Run(closure)` and returns the `Task` that `Run` returns. The consumer itself is NOT `cil managed async` — only `Run` and the CE body closures are. + +### How It Works (Technical Details) + +#### The Non-Inline Run Pattern + +1. `Run` is `member _` (non-inline) with `[]` — the compiler emits it as `cil managed async` +2. CE body closures contain `AsyncHelpers.Await` calls (from inlined `Bind` members, or the auto-injected sentinel) — they are also `cil managed async` +3. At runtime, the closure's `Invoke` returns `Task<'T>` (because it's `cil managed async` — the runtime wraps `'T → Task<'T>` automatically) +4. The compiler handles `'T → Task<'T>` bridging automatically for `[]` builders — no `cast` helper is needed +5. `AsyncHelpers.Await(Task<'T>)` unwraps `Task<'T>` to `'T` +6. `Run` wraps `'T` back to `Task<'T>` (because it's `cil managed async`) + +#### True Inline-Nested CEs + +Because `Run` is non-inline and returns a real `Task`, `runtimeTask { ... }` CEs can be nested directly inside each other: + +```fsharp +let trueInlineNestedRuntimeTask () : Task = + runtimeTask { + let! a = + runtimeTask { + return 21 + } + let! b = + runtimeTask { + return 21 + } + return a + b // 42 + } +``` + +The inner `runtimeTask { return 21 }` calls `Run` which returns a real `Task`. The outer `Bind` calls `AsyncHelpers.Await(Task)` → gets 21. This works because `Run` is a real `cil managed async` method, not an inlined cast. + +#### Three Required Compiler Fixes + +**Fix 1 — IlxGen.fs return-type guard:** `ExprContainsAsyncHelpersAwaitCall` body analysis must only propagate `cil managed async` when the method returns a Task-like type (`Task`, `Task`, `ValueTask`, `ValueTask`). Without this guard, the optimizer might inline an async function into a non-Task-returning method (e.g., `main : int`), and the runtime would reject it with `TypeLoadException`. + +**Fix 2 — Optimizer.fs anti-inlining guard:** Functions whose optimized bodies contain `AsyncHelpers.Await`/`AwaitAwaiter`/`UnsafeAwaitAwaiter` calls must not be cross-module inlined by the optimizer. Their optimization data is replaced with `UnknownValue`. Without this, the optimizer inlines async functions into non-async callers, causing `NullReferenceException` at runtime. + +**Fix 3 — EraseClosures.fs async closure emission:** CE body closures contain `AsyncHelpers.Await` calls (from inlined `Bind` members, or the auto-injected sentinel). The `cloIsAsync` field in `IlxClosureInfo` is set when the closure body contains these calls. `EraseClosures.fs` emits the closure's `Invoke` method as `cil managed async` when `cloIsAsync = true`. Without this, the runtime rejects the closure with `TypeLoadException` because `AsyncHelpers.Await` can only be called from `cil managed async` methods. + +**Fix 4 — CheckExpressions.fs type-checking coercion:** When inside an inline member of a `[]` type, the compiler allows `fun () -> f()` where `f()` returns `'T` but the closure's declared return type is `Task<'T>`. The compiler unwraps the Task-like return type for the lambda body, so the library author writes `fun () -> f()` without any cast helper. + +**Fix 5 — IlxGen.fs Lambdas_return fix:** When the closure's declared return type is `Task<'T>` but the body type is `'T` (due to the coercion in Fix 4), the IL generator uses the declared `Task<'T>` for `Lambdas_return` so the closure's `Invoke` method declares the correct return type in IL. + +**Fix 6 — CheckComputationExpressions.fs automatic sentinel injection:** When the builder type has `[]`, the CE desugaring automatically injects `AsyncHelpers.Await(ValueTask.CompletedTask)` as the first expression in ALL `Delay` closure bodies. This ensures `cloIsAsync = true` even when the CE body has no `let!`/`do!` bindings (e.g., `runtimeTask { return 42 }`), so the closure is always emitted as `cil managed async`. + +### Examples + +The sample includes 12 examples in `Api.fs`: + +| Example | Demonstrates | +|---|---| +| `addFromTaskAndValueTask` | Binding `Task` and `ValueTask` | +| `bindUnitTaskAndUnitValueTask` | Binding unit `Task` and unit `ValueTask` via `do!` | +| `safeDivide` | `try/with` inside runtimeTask | +| `nestedRuntimeTask` | Composing runtimeTask functions | +| `deeplyNestedRuntimeTask` | 3-level deep nesting via helper functions | +| `consumeOlderTaskCE` | Consuming standard `task { }` CE results | +| `taskDelayYieldAndRun` | `Task.Delay`, `Task.Yield()` (generic awaitable), `Task.Run` | +| `useAsyncDisposable` | `use` with `IAsyncDisposable` resource | +| `iterateAsyncEnumerable` | `for` over `IAsyncEnumerable` | +| `configureAwaitExample` | `.ConfigureAwait(false)` on Task and Task | +| `inlineNestedRuntimeTask` | Nesting runtimeTask CEs via separate functions | +| `trueInlineNestedRuntimeTask` | True inline-nested runtimeTask CEs (enabled by non-inline Run) | + +### Prerequisites + +- .NET 10 SDK +- F# preview language enabled (already set in each project) +- .NET SDK restore access (normal `dotnet run` prerequisites) +- `DOTNET_RuntimeAsync=1` set before launching the process (required for loading runtime-async methods) + +### Build + +```bash +dotnet build docs/samples/runtime-async-library/RuntimeAsync.Demo/RuntimeAsync.Demo.fsproj -c Release +``` + +### Run + +**Linux/macOS:** +```bash +DOTNET_RuntimeAsync=1 dotnet run --project docs/samples/runtime-async-library/RuntimeAsync.Demo/RuntimeAsync.Demo.fsproj -c Release +``` + +**Windows (PowerShell):** +```powershell +$env:DOTNET_RuntimeAsync = "1" +dotnet run --project docs/samples/runtime-async-library/RuntimeAsync.Demo/RuntimeAsync.Demo.fsproj -c Release +``` + +### Expected Output + +``` +Task + ValueTask -> 15 +Task + ValueTask -> completed +try/with -> 0 +nested runtimeTask -> 44 +deeply nested runtimeTask -> 100 +consume older task CE -> 84 +Task.Delay + Task.Yield + Task.Run -> 42 +IAsyncDisposable -> async resource used +IAsyncEnumerable sum -> 15 +ConfigureAwait(false) -> 99 +inline-nested runtimeTask -> 40 +true inline-nested runtimeTask -> 42 +``` + +### IL Verification + +Both projects have an `ILDasm.targets` file that runs ILDasm after build, producing `.il` files in their respective output directories. + +To verify manually: + +```bash +# Build the library +dotnet build docs/samples/runtime-async-library/RuntimeAsync.Library/RuntimeAsync.Library.fsproj -c Release +``` + +In the output IL: + +- `RuntimeTaskBuilder::Run` → should show `cil managed async` (non-inline, has `[]`) +- CE body closures (e.g., `addFromTaskAndValueTask@57`) → `Invoke` method should show `cil managed async` (contains `AsyncHelpers.Await` calls) +- `Api::*` consumer functions → should show `cil managed` (NOT `cil managed async` — they just call `Run` and return the `Task`) +- `Program::main` → should show `cil managed` (NOT `cil managed async`) + +> **Note:** Running without `DOTNET_RuntimeAsync=1` fails with `TypeLoadException` because runtime-async methods are not enabled for that process. diff --git a/docs/samples/runtime-async-library/RuntimeAsync.Demo/Program.fs b/docs/samples/runtime-async-library/RuntimeAsync.Demo/Program.fs new file mode 100644 index 00000000000..5475a21d249 --- /dev/null +++ b/docs/samples/runtime-async-library/RuntimeAsync.Demo/Program.fs @@ -0,0 +1,32 @@ +open System.Threading.Tasks +open RuntimeAsync.Library + +[] +let main _ = + let fromTaskLike = (Api.addFromTaskAndValueTask (Task.FromResult 10) (ValueTask(5))).Result + let fromUnitTasks = (Api.bindUnitTaskAndUnitValueTask ()).Result + let fromTryWith = (Api.safeDivide 10 0).Result + let fromNested = (Api.nestedRuntimeTask ()).Result + let fromDeepNested = (Api.deeplyNestedRuntimeTask ()).Result + let fromOlderCE = (Api.consumeOlderTaskCE ()).Result + let fromDelayYieldRun = (Api.taskDelayYieldAndRun ()).Result + let fromAsyncDisposable = (Api.useAsyncDisposable ()).Result + let fromAsyncEnumerable = (Api.iterateAsyncEnumerable ()).Result + let fromConfigureAwait = (Api.configureAwaitExample ()).Result + let fromInlineNested = (Api.inlineNestedRuntimeTask ()).Result + let fromTrueInlineNested = (Api.trueInlineNestedRuntimeTask ()).Result + + printfn "Task + ValueTask -> %d" fromTaskLike + printfn "Task + ValueTask -> %s" fromUnitTasks + printfn "try/with -> %d" fromTryWith + printfn "nested runtimeTask -> %d" fromNested + printfn "deeply nested runtimeTask -> %d" fromDeepNested + printfn "consume older task CE -> %d" fromOlderCE + printfn "Task.Delay + Task.Yield + Task.Run -> %d" fromDelayYieldRun + printfn "IAsyncDisposable -> %s" fromAsyncDisposable + printfn "IAsyncEnumerable sum -> %d" fromAsyncEnumerable + printfn "ConfigureAwait(false) -> %d" fromConfigureAwait + printfn "inline-nested runtimeTask -> %d" fromInlineNested + printfn "true inline-nested runtimeTask -> %d" fromTrueInlineNested + + 0 diff --git a/docs/samples/runtime-async-library/RuntimeAsync.Demo/RuntimeAsync.Demo.fsproj b/docs/samples/runtime-async-library/RuntimeAsync.Demo/RuntimeAsync.Demo.fsproj new file mode 100644 index 00000000000..aef347f8c8d --- /dev/null +++ b/docs/samples/runtime-async-library/RuntimeAsync.Demo/RuntimeAsync.Demo.fsproj @@ -0,0 +1,24 @@ + + + Exe + net10.0 + preview + $(MSBuildThisFileDirectory)..\..\..\..\artifacts\bin\fsc\Release\net10.0\ + fsc.exe + true + + + + + + + + + + + + + + + + diff --git a/docs/samples/runtime-async-library/RuntimeAsync.Library/Api.fs b/docs/samples/runtime-async-library/RuntimeAsync.Library/Api.fs new file mode 100644 index 00000000000..f4991f999ce --- /dev/null +++ b/docs/samples/runtime-async-library/RuntimeAsync.Library/Api.fs @@ -0,0 +1,199 @@ +namespace RuntimeAsync.Library + +open System +open System.Collections.Generic +open System.Threading.Tasks + +// --------------------------------------------------------------------------- +// Helper types for IAsyncDisposable and IAsyncEnumerable examples +// --------------------------------------------------------------------------- + +/// A simple type that only implements IAsyncDisposable (not IDisposable) +/// for demonstrating async resource cleanup in runtimeTask. +type SimpleAsyncResource() = + let mutable disposed = false + member _.IsDisposed = disposed + + member _.DoWorkAsync() = Task.FromResult "async resource used" + + interface IAsyncDisposable with + member _.DisposeAsync() = + disposed <- true + ValueTask.CompletedTask + +/// A simple IAsyncEnumerable that yields integers from start to start+count-1. +type AsyncRange(start: int, count: int) = + interface IAsyncEnumerable with + member _.GetAsyncEnumerator(_ct) = + let mutable current = start - 1 + let mutable remaining = count + + { new IAsyncEnumerator with + member _.Current = current + + member _.MoveNextAsync() = + if remaining > 0 then + current <- current + 1 + remaining <- remaining - 1 + ValueTask(true) + else + ValueTask(false) + + member _.DisposeAsync() = ValueTask.CompletedTask + } + +// --------------------------------------------------------------------------- +// API examples — all use runtimeTask with NO [] +// --------------------------------------------------------------------------- + +module Api = + + // === Existing examples === + + let addFromTaskAndValueTask (left: Task) (right: ValueTask) : Task = + runtimeTask { + let! l = left + let! r = right + return l + r + } + + let bindUnitTaskAndUnitValueTask () : Task = + runtimeTask { + do! Task.CompletedTask + do! ValueTask.CompletedTask + return "completed" + } + + let safeDivide (x: int) (y: int) : Task = + runtimeTask { + try + if y = 0 then + failwith "division by zero" + + return x / y + with _ -> + return 0 + } + + let nestedRuntimeTask () : Task = + runtimeTask { + let! x = addFromTaskAndValueTask (Task.FromResult 20) (ValueTask(22)) + return x + 2 + } + + // === Nested runtimeTask CEs (3 levels deep) === + // Each nesting level must be a separate function so that each gets its own 'cil managed async' + // method. Inline-nested runtimeTask CEs are not supported because the intermediate cast values + // are not real Tasks and cannot be consumed by Bind's AsyncHelpers.Await. + + let private innerInnerTask () : Task = + runtimeTask { + return 10 + } + + let private innerTask () : Task = + runtimeTask { + let! b = innerInnerTask () + return b + 20 + } + + let deeplyNestedRuntimeTask () : Task = + runtimeTask { + let! a = innerTask () + return a + 70 + } + + // === Consuming tasks from the standard FSharp.Core task CE === + + let consumeOlderTaskCE () : Task = + // Create a task using the standard state-machine based task CE from FSharp.Core + let standardTask = + task { + do! Task.Delay(1) + return 42 + } + + // Consume it in runtimeTask (runtime-async, no state machine) + runtimeTask { + let! result = standardTask + return result * 2 + } + + // === Task.Delay, Task.Yield, Task.Run === + + let taskDelayYieldAndRun () : Task = + runtimeTask { + // Task.Delay returns Task — bound via do! + do! Task.Delay(5000) + // Task.Yield() returns YieldAwaitable — bound via the generic awaitable Bind extension + do! Task.Yield() + // Task.Run returns Task — bound via let! + let! fromRun = Task.Run(fun () -> 7 * 6) + return fromRun + } + + // === IAsyncDisposable === + + let useAsyncDisposable () : Task = + runtimeTask { + use resource = new SimpleAsyncResource() + let! result = resource.DoWorkAsync() + return result + } + + // === IAsyncEnumerable === + + let iterateAsyncEnumerable () : Task = + runtimeTask { + let mutable sum = 0 + + for x in AsyncRange(1, 5) do + sum <- sum + x + + return sum + } + + // === ConfigureAwait === + + let configureAwaitExample () : Task = + runtimeTask { + // ConfigureAwait(false) returns ConfiguredTaskAwaitable — bound via intrinsic Bind + let! value = (Task.FromResult 99).ConfigureAwait(false) + // ConfigureAwait on unit Task returns ConfiguredTaskAwaitable — bound via intrinsic Bind + do! Task.CompletedTask.ConfigureAwait(false) + return value + } + + // === Inline-nested runtimeTask CEs === + // With non-inline Run ([]), a runtimeTask CE can be nested + // directly inside another runtimeTask CE. The inner runtimeTask { ... } call invokes Run + // which returns a real Task that the outer CE's Bind can AsyncHelpers.Await. + // Each nesting level can be a separate function OR inline — both work with non-inline Run. + // (With the old inline Run + cast design, inline-nested CEs did NOT work.) + let inlineNestedRuntimeTask () : Task = + runtimeTask { + // Calling separate functions that return Task — this works because each function + // is a real 'cil managed async' method returning a real Task (not a fake cast value). + let! a = innerInnerTask () + let! b = innerTask () + return a + b + } + + // === True inline-nested runtimeTask CEs === + // With non-inline Run, runtimeTask CEs can be nested directly inside each other in the same + // function. The inner runtimeTask { ... } calls Run which returns a real Task that the + // outer CE's Bind can AsyncHelpers.Await — no separate helper functions needed. + let trueInlineNestedRuntimeTask () : Task = + runtimeTask { + let! a = + runtimeTask { + do! Task.Yield() + return 21 + } + let! b = + runtimeTask { + do! Task.Yield() + return 21 + } + return a + b + } diff --git a/docs/samples/runtime-async-library/RuntimeAsync.Library/RuntimeAsync.Library.fsproj b/docs/samples/runtime-async-library/RuntimeAsync.Library/RuntimeAsync.Library.fsproj new file mode 100644 index 00000000000..7ebd6bf251c --- /dev/null +++ b/docs/samples/runtime-async-library/RuntimeAsync.Library/RuntimeAsync.Library.fsproj @@ -0,0 +1,21 @@ + + + net10.0 + preview + true + $(MSBuildThisFileDirectory)..\..\..\..\artifacts\bin\fsc\Release\net10.0\ + fsc.exe + true + + + + + + + + + + + + + diff --git a/docs/samples/runtime-async-library/RuntimeAsync.Library/RuntimeTaskBuilder.fs b/docs/samples/runtime-async-library/RuntimeAsync.Library/RuntimeTaskBuilder.fs new file mode 100644 index 00000000000..db4e1e7929b --- /dev/null +++ b/docs/samples/runtime-async-library/RuntimeAsync.Library/RuntimeTaskBuilder.fs @@ -0,0 +1,187 @@ +namespace RuntimeAsync.Library + +open System +open System.Collections.Generic +open System.Runtime.CompilerServices +open System.Threading +open System.Threading.Tasks + +open Microsoft.FSharp.Control + +#nowarn "57" + +/// Computation expression builder for runtime-async methods. +/// Annotated with [] so the compiler: +/// (1) Implicitly applies NoDynamicInvocation to all public inline members +/// (2) Gates optimizer anti-inlining behind this attribute +/// +/// Design (Architecture B): Delay creates a closure that wraps the CE body. +/// [] on f inlines the CE body into the Delay closure, so there is only ONE +/// closure containing all AsyncHelpers.Await calls. The compiler automatically injects a sentinel +/// to ensure the Delay closure is always 'cil managed async', and automatically handles the +/// 'T → Task<'T> bridging for [] builders — no cast is needed. +/// Run is non-inline with [] and takes unit -> Task<'T>. +/// This enables true inline-nested runtimeTask { ... } CEs. +[] +type RuntimeTaskBuilder() = + member inline _.Return(x: 'T) : 'T = x + + member inline _.Bind(t: Task<'T>, [] f: 'T -> 'U) : 'U = + f(AsyncHelpers.Await t) + + member inline _.Bind(t: Task, [] f: unit -> 'U) : 'U = + AsyncHelpers.Await t + f() + + member inline _.Bind(t: ValueTask<'T>, [] f: 'T -> 'U) : 'U = + f(AsyncHelpers.Await t) + + member inline _.Bind(t: ValueTask, [] f: unit -> 'U) : 'U = + AsyncHelpers.Await t + f() + + // ConfiguredTaskAwaitable — allows task.ConfigureAwait(false) in runtimeTask + member inline _.Bind(cta: ConfiguredTaskAwaitable, [] f: unit -> 'U) : 'U = + AsyncHelpers.Await cta + f() + + member inline _.Bind(cta: ConfiguredTaskAwaitable<'T>, [] f: 'T -> 'U) : 'U = + f(AsyncHelpers.Await cta) + + // ConfiguredValueTaskAwaitable — allows valueTask.ConfigureAwait(false) in runtimeTask + member inline _.Bind(cvta: ConfiguredValueTaskAwaitable, [] f: unit -> 'U) : 'U = + AsyncHelpers.Await cvta + f() + + member inline _.Bind(cvta: ConfiguredValueTaskAwaitable<'T>, [] f: 'T -> 'U) : 'U = + f(AsyncHelpers.Await cvta) + + /// Delay creates a closure that wraps the CE body. + /// [] on f inlines the CE body into the Delay closure, so there is only ONE + /// closure containing all AsyncHelpers.Await calls. The compiler automatically injects a + /// sentinel to ensure the Delay closure is always 'cil managed async', even when the CE body + /// has no let!/do! bindings. The compiler also handles the 'T → Task<'T> bridging automatically + /// for [] builders, so no cast is needed. + member inline _.Delay([] f: unit -> 'T) : unit -> Task<'T> = + fun () -> f() + + member inline _.Zero() : unit = () + /// Combine sequences two CE expressions. The second expression is wrapped in Delay, + /// so f returns unit -> Task<'T>. f must NOT be []: if it were, the Delay + /// closure body would be inlined and f() would push 'T (not Task<'T>) at IL level, making + /// AsyncHelpers.Await(f()) fail (Await expects Task<'T> but gets 'T). By not inlining f, + /// f() calls the Delay closure Invoke → Task<'T> via cil managed async, so Await works. + member inline _.Combine((): unit, f: unit -> Task<'T>) : 'T = + AsyncHelpers.Await(f()) + + /// While loops. The body is wrapped in Delay, so body returns unit -> Task. + /// Each iteration awaits body() so the async body completes before the next iteration. + member inline _.While([] guard: unit -> bool, body: unit -> Task) : unit = + while guard() do + AsyncHelpers.Await(body()) + + /// TryWith handles try/with in CEs. The body is wrapped in Delay (unit -> Task<'T>). + /// We await body() inside the try block so that exceptions from the async body are caught + /// by the with clause. Returns 'T (not Task<'T>) — the outer Delay closure wraps in Task<'T>. + member inline _.TryWith(body: unit -> Task<'T>, [] handler: exn -> 'T) : 'T = + try + // Await body() inside try so async exceptions are caught by the with clause. + AsyncHelpers.Await(body()) + with e -> + handler e + + /// TryFinally handles try/finally in CEs. The body is wrapped in Delay (unit -> Task<'T>). + /// We await body() inside the try block so the finally clause runs after async completion. + /// Returns 'T (not Task<'T>) — the outer Delay closure wraps in Task<'T>. + member inline _.TryFinally(body: unit -> Task<'T>, [] comp: unit -> unit) : 'T = + try + // Await body() inside try so the finally clause runs after async completion. + AsyncHelpers.Await(body()) + finally + comp() + + /// TryFinally with async compensation — awaits a ValueTask in the finally block. + /// Used by Using(IAsyncDisposable) to await DisposeAsync(). + member inline _.TryFinallyAsync + ([] body: unit -> 'T, [] compensation: unit -> ValueTask) + : 'T = + try + body() + finally + AsyncHelpers.Await(compensation()) + + /// IAsyncDisposable — intrinsic member so it is preferred over the IDisposable extension + /// when a type implements both interfaces. + member inline this.Using + (resource: 'T when 'T :> IAsyncDisposable, [] body: 'T -> 'U) + : 'U = + this.TryFinallyAsync( + (fun () -> body resource), + (fun () -> + if not (isNull (box resource)) then + resource.DisposeAsync() + else + ValueTask.CompletedTask + ) + ) + + /// IAsyncEnumerable — intrinsic member so it is preferred over the seq extension. + /// Awaits MoveNextAsync() and DisposeAsync() on the enumerator. + member inline _.For(sequence: IAsyncEnumerable<'T>, [] body: 'T -> unit) : unit = + let enumerator = sequence.GetAsyncEnumerator(CancellationToken.None) + + try + while AsyncHelpers.Await(enumerator.MoveNextAsync()) do + body(enumerator.Current) + finally + AsyncHelpers.Await(enumerator.DisposeAsync()) + + /// Run is non-inline with [] — the compiler emits it as + /// 'cil managed async'. Run takes the Delay closure (unit -> Task<'T>) and awaits it. + /// The Delay closure is 'cil managed async' and returns Task<'T> at runtime. + /// Run awaits f() to get Task<'T>, then AsyncHelpers.Await unwraps it to 'T, then Run + /// wraps 'T back to Task<'T>. This enables true inline-nested runtimeTask { ... } CEs. + [ 0x2000)>] + member _.Run(f: unit -> Task<'T>) : Task<'T> = + // f() returns Task<'T> (the Delay closure is 'cil managed async'). + // AsyncHelpers.Await unwraps Task<'T> to 'T. Run wraps 'T back to Task<'T>. + AsyncHelpers.Await(f()) + +/// IDisposable Using and seq For as type extensions. +/// These have lower priority than the intrinsic IAsyncDisposable/IAsyncEnumerable members above, +/// so when a type implements both IDisposable and IAsyncDisposable, the async variant wins. +[] +module RuntimeTaskBuilderExtensions = + type RuntimeTaskBuilder with + + member inline _.Using + (resource: 'T when 'T :> IDisposable, [] body: 'T -> 'U) + : 'U = + try + body resource + finally + (resource :> IDisposable).Dispose() + + [] + member inline _.For(s: seq<'T>, [] body: 'T -> unit) : unit = + for x in s do + body(x) + + /// Generic Bind for any awaitable type that has a GetAwaiter() method returning + /// an awaiter implementing ICriticalNotifyCompletion. + /// This handles types like YieldAwaitable, custom awaitables, etc. + /// Lower priority than the intrinsic Bind overloads for Task/ValueTask/ConfiguredTask. + [] + member inline _.Bind(awaitable: ^Awaitable, [] f: ^TResult -> 'U) : 'U + when ^Awaitable : (member GetAwaiter: unit -> ^Awaiter) + and ^Awaiter :> ICriticalNotifyCompletion + and ^Awaiter : (member get_IsCompleted: unit -> bool) + and ^Awaiter : (member GetResult: unit -> ^TResult) = + let awaiter = (^Awaitable : (member GetAwaiter: unit -> ^Awaiter) awaitable) + if not ((^Awaiter : (member get_IsCompleted: unit -> bool) awaiter)) then + AsyncHelpers.UnsafeAwaitAwaiter(awaiter) + f ((^Awaiter : (member GetResult: unit -> ^TResult) awaiter)) + +[] +module RuntimeTaskBuilderModule = + let runtimeTask = RuntimeTaskBuilder() diff --git a/docs/samples/runtime-async-library/kill-dotnet.ps1 b/docs/samples/runtime-async-library/kill-dotnet.ps1 new file mode 100644 index 00000000000..2ae238ce12e --- /dev/null +++ b/docs/samples/runtime-async-library/kill-dotnet.ps1 @@ -0,0 +1,55 @@ +<# +.SYNOPSIS + Continuously kills dotnet.exe processes. + +.DESCRIPTION + Runs in a loop, checking every second for dotnet.exe processes and killing them. + Optionally filters processes by their command line arguments. + +.PARAMETER ArgumentFilter + Optional filter to match against the command line arguments of dotnet.exe processes. + Only processes whose command line contains this string will be killed. + Example: "MyFoo.dll" will kill processes running "dotnet.exe MyFoo.dll" + +.PARAMETER MaxTime + Maximum time in seconds to run the loop. Default is 30 seconds. + Use -1 to run indefinitely. + +.EXAMPLE + .\kill-dotnet.ps1 + Kills all dotnet.exe processes for 30 seconds. + +.EXAMPLE + .\kill-dotnet.ps1 -ArgumentFilter "MyFoo.dll" + Kills only dotnet.exe processes that have "MyFoo.dll" in their command line. + +.EXAMPLE + .\kill-dotnet.ps1 -MaxTime -1 + Kills all dotnet.exe processes indefinitely. + +.EXAMPLE + .\kill-dotnet.ps1 -MaxTime 60 -ArgumentFilter "MyFoo.dll" + Kills matching processes for 60 seconds. + +.NOTES + Press Ctrl+C to stop the script. +#> +param( + [string]$ArgumentFilter, + [int]$MaxTime = 30 +) + +$startTime = Get-Date + +while ($MaxTime -eq -1 -or ((Get-Date) - $startTime).TotalSeconds -lt $MaxTime) { + $processes = Get-CimInstance Win32_Process -Filter "Name = 'dotnet.exe'" -ErrorAction SilentlyContinue + if ($ArgumentFilter) { + $processes = $processes | Where-Object { $_.CommandLine -like "*$ArgumentFilter*" } + } + $count = ($processes | Measure-Object).Count + if ($count -gt 0) { + $processes | ForEach-Object { Stop-Process -Id $_.ProcessId -Force } + Write-Host "Killed $count dotnet process(es)" + } + Start-Sleep -Seconds 1 +} diff --git a/docs/samples/runtime-async-library/kill-dotnet.sh b/docs/samples/runtime-async-library/kill-dotnet.sh new file mode 100644 index 00000000000..9a547c43b56 --- /dev/null +++ b/docs/samples/runtime-async-library/kill-dotnet.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +# ============================================================================= +# kill-dotnet.sh +# Continuously kills dotnet processes. +# +# SYNOPSIS +# ./kill-dotnet.sh [OPTIONS] +# +# DESCRIPTION +# Runs in an infinite loop, checking every second for dotnet processes +# and killing them. Optionally filters processes by their command line +# arguments. Supports both macOS and Linux. +# +# OPTIONS +# -f, --filter PATTERN +# Optional filter to match against the command line arguments of +# dotnet processes. Only processes whose command line contains this +# string will be killed. +# Example: -f "MyFoo.dll" will kill processes running "dotnet MyFoo.dll" +# +# -t, --max-time SECONDS +# Maximum time in seconds to run the loop. Default is 30 seconds. +# Use -1 to run indefinitely. +# +# -h, --help +# Display this help message and exit. +# +# EXAMPLES +# ./kill-dotnet.sh +# Kills all dotnet processes for 30 seconds. +# +# ./kill-dotnet.sh -f "MyFoo.dll" +# Kills only dotnet processes that have "MyFoo.dll" in their command line. +# +# ./kill-dotnet.sh -t -1 +# Kills all dotnet processes indefinitely. +# +# ./kill-dotnet.sh -t 60 -f "MyFoo.dll" +# Kills matching processes for 60 seconds. +# +# NOTES +# Press Ctrl+C to stop the script. +# ============================================================================= + +show_help() { + sed -n '3,44p' "$0" | sed 's/^# //' | sed 's/^#//' +} + +ARGUMENT_FILTER="" +MAX_TIME=30 + +while [[ $# -gt 0 ]]; do + case $1 in + -f|--filter) + ARGUMENT_FILTER="$2" + shift 2 + ;; + -t|--max-time) + MAX_TIME="$2" + shift 2 + ;; + -h|--help) + show_help + exit 0 + ;; + *) + echo "Unknown option: $1" + echo "Use -h or --help for usage information." + exit 1 + ;; + esac +done + +START_TIME=$(date +%s) + +while true; do + if [[ $MAX_TIME -ne -1 ]]; then + CURRENT_TIME=$(date +%s) + ELAPSED=$((CURRENT_TIME - START_TIME)) + if [[ $ELAPSED -ge $MAX_TIME ]]; then + break + fi + fi + if [[ -n "$ARGUMENT_FILTER" ]]; then + # Filter by argument pattern + pids=$(ps aux | grep -E '[d]otnet' | grep "$ARGUMENT_FILTER" | awk '{print $2}') + else + # All dotnet processes + pids=$(pgrep -x dotnet 2>/dev/null || pgrep dotnet 2>/dev/null) + fi + + if [[ -n "$pids" ]]; then + count=$(echo "$pids" | wc -l | tr -d ' ') + echo "$pids" | xargs kill -9 2>/dev/null + echo "Killed $count dotnet process(es)" + fi + + sleep 1 +done diff --git a/eng/Versions.props b/eng/Versions.props index 65aa6ffe361..0217ec4f023 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -163,7 +163,7 @@ 1.0.31 4.3.0-1.22220.8 - 5.0.0-preview.7.20364.11 + 10.0.0 5.0.0-preview.7.20364.11 17.14.1 2.0.2 diff --git a/src/Compiler/AbstractIL/il.fs b/src/Compiler/AbstractIL/il.fs index 5d7848f246e..79f37d094ae 100644 --- a/src/Compiler/AbstractIL/il.fs +++ b/src/Compiler/AbstractIL/il.fs @@ -2114,6 +2114,11 @@ type ILMethodDef member x.IsMustRun = x.ImplAttributes &&& MethodImplAttributes.NoOptimization <> enum 0 + // Async is defined as 0x2000 or 8192 + // https://github.com/dotnet/runtime/blob/main/docs/design/specs/runtime-async.md + member x.IsAsync = + x.ImplAttributes &&& enum (0x2000) <> enum 0 + member x.WithSpecialName = x.With(attributes = (x.Attributes ||| MethodAttributes.SpecialName)) @@ -2170,6 +2175,9 @@ type ILMethodDef |> conditionalAdd condition MethodImplAttributes.AggressiveInlining) ) + member x.WithAsync(condition) = + x.With(implAttributes = (x.ImplAttributes |> conditionalAdd condition (enum 0x2000))) + member x.WithRuntime(condition) = x.With(implAttributes = (x.ImplAttributes |> conditionalAdd condition MethodImplAttributes.Runtime)) diff --git a/src/Compiler/AbstractIL/il.fsi b/src/Compiler/AbstractIL/il.fsi index 3d6f88bb6ca..860892f33f5 100644 --- a/src/Compiler/AbstractIL/il.fsi +++ b/src/Compiler/AbstractIL/il.fsi @@ -1157,6 +1157,9 @@ type ILMethodDef = /// SafeHandle finalizer must be run. member IsMustRun: bool + /// https://github.com/dotnet/runtime/blob/main/docs/design/specs/runtime-async.md + member IsAsync: bool + /// Functional update of the value member internal With: ?name: string * @@ -1200,6 +1203,8 @@ type ILMethodDef = member internal WithAggressiveInlining: bool -> ILMethodDef + member internal WithAsync: bool -> ILMethodDef + member internal WithRuntime: bool -> ILMethodDef /// Tables of methods. Logically equivalent to a list of methods but diff --git a/src/Compiler/AbstractIL/ilprint.fs b/src/Compiler/AbstractIL/ilprint.fs index 12e421f6829..927557f2b30 100644 --- a/src/Compiler/AbstractIL/ilprint.fs +++ b/src/Compiler/AbstractIL/ilprint.fs @@ -617,6 +617,9 @@ let goutput_mbody is_entrypoint env os (md: ILMethodDef) = output_string os (if md.IsManaged then "managed " else " ") + if md.IsAsync then + output_string os "async " + output_string os (if md.IsForwardRef then "forwardref " else " ") output_string os " \n{ \n" diff --git a/src/Compiler/AbstractIL/ilx.fs b/src/Compiler/AbstractIL/ilx.fs index cc911ef6d48..973936bd442 100644 --- a/src/Compiler/AbstractIL/ilx.fs +++ b/src/Compiler/AbstractIL/ilx.fs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. /// Defines an extension of the IL algebra module internal FSharp.Compiler.AbstractIL.ILX.Types @@ -168,6 +168,9 @@ type IlxClosureInfo = cloFreeVars: IlxClosureFreeVar[] cloCode: InterruptibleLazy cloUseStaticField: bool + /// If true, the Invoke method for this closure should be emitted as 'cil managed async'. + /// Set when the closure body contains AsyncHelpers.Await calls (detected in IlxGen.fs). + cloIsAsync: bool } type IlxUnionInfo = diff --git a/src/Compiler/AbstractIL/ilx.fsi b/src/Compiler/AbstractIL/ilx.fsi index 28122885f8e..b0e286f3041 100644 --- a/src/Compiler/AbstractIL/ilx.fsi +++ b/src/Compiler/AbstractIL/ilx.fsi @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. /// ILX extensions to Abstract IL types and instructions F# module internal FSharp.Compiler.AbstractIL.ILX.Types @@ -120,7 +120,10 @@ type IlxClosureInfo = { cloStructure: IlxClosureLambdas cloFreeVars: IlxClosureFreeVar[] cloCode: InterruptibleLazy - cloUseStaticField: bool } + cloUseStaticField: bool + /// If true, the Invoke method for this closure should be emitted as 'cil managed async'. + /// Set when the closure body contains AsyncHelpers.Await calls (detected in IlxGen.fs). + cloIsAsync: bool } /// Represents a discriminated union type prior to erasure type IlxUnionInfo = diff --git a/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs b/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs index 0af55834f8f..6946d3a8523 100644 --- a/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckComputationExpressions.fs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. /// The typechecker. Left-to-right constrained type checking /// with generalization at appropriate points. @@ -85,6 +85,48 @@ let inline addBindDebugPoint spBind e = let inline mkSynDelay2 (e: SynExpr) = mkSynDelay (e.Range.MakeSynthetic()) e +/// Check if the builder type has [] attribute. +/// Used to determine whether to inject the AsyncHelpers.Await sentinel into Delay closures. +let builderHasRuntimeAsync ceenv = + match tryTcrefOfAppTy ceenv.cenv.g ceenv.builderTy with + | ValueSome tcref -> + TryFindFSharpAttribute ceenv.cenv.g ceenv.cenv.g.attrib_RuntimeAsyncAttribute tcref.Attribs + |> Option.isSome + | ValueNone -> false + +/// Create the sentinel expression: AsyncHelpers.Await(ValueTask.CompletedTask) +/// This is a no-op at runtime but its IL presence forces cloIsAsync = true for the enclosing closure. +let mkRuntimeAsyncSentinelExpr (m: range) = + let m = m.MakeSynthetic() + + let awaitFunc = + mkSynLidGet + m + [ "System"; "Runtime"; "CompilerServices"; "AsyncHelpers" ] + "Await" + + let completedTask = + mkSynLidGet + m + [ "System"; "Threading"; "Tasks"; "ValueTask" ] + "CompletedTask" + + SynExpr.App(ExprAtomicFlag.NonAtomic, false, awaitFunc, completedTask, m) + +/// Wrap a Delay closure body with the RuntimeAsync sentinel as the first statement. +/// Produces: sentinel; body +let wrapWithRuntimeAsyncSentinel (bodyExpr: SynExpr) = + let m = bodyExpr.Range.MakeSynthetic() + + SynExpr.Sequential( + DebugPointAtSequential.SuppressNeither, + true, + mkRuntimeAsyncSentinelExpr m, + bodyExpr, + m, + SynExprSequentialTrivia.Zero + ) + /// Make a builder.Method(...) call let mkSynCall nm (m: range) args builderValName = let m = m.MakeSynthetic() // Mark as synthetic so the language service won't pick it up. @@ -1399,7 +1441,14 @@ let rec TryTranslateComputationExpression mWhile [ mkSynDelay2 guardExpr - mkSynCall "Delay" mWhile [ mkSynDelay innerComp.Range holeFill ] ceenv.builderValName + // For [] builders, inject the sentinel so the Delay closure is cil managed async. + let whileDelayBody = + if builderHasRuntimeAsync ceenv then + wrapWithRuntimeAsyncSentinel holeFill + else + holeFill + + mkSynCall "Delay" mWhile [ mkSynDelay innerComp.Range whileDelayBody ] ceenv.builderValName ] ceenv.builderValName )) @@ -1579,7 +1628,14 @@ let rec TryTranslateComputationExpression "TryFinally" mTry [ - mkSynCall "Delay" mTry [ mkSynDelay innerComp.Range innerExpr ] ceenv.builderValName + // For [] builders, inject the sentinel so the Delay closure is cil managed async. + let tryFinallyDelayBody = + if builderHasRuntimeAsync ceenv then + wrapWithRuntimeAsyncSentinel innerExpr + else + innerExpr + + mkSynCall "Delay" mTry [ mkSynDelay innerComp.Range tryFinallyDelayBody ] ceenv.builderValName mkSynDelay2 unwindExpr2 ] ceenv.builderValName @@ -1706,7 +1762,16 @@ let rec TryTranslateComputationExpression "Delay" m1 [ - mkSynDelay innerComp2.Range (TranslateComputationExpressionNoQueryOps ceenv innerComp2) + // For [] builders, inject the sentinel so the Delay closure is cil managed async. + let innerComp2Translated = TranslateComputationExpressionNoQueryOps ceenv innerComp2 + + let innerComp2Body = + if builderHasRuntimeAsync ceenv then + wrapWithRuntimeAsyncSentinel innerComp2Translated + else + innerComp2Translated + + mkSynDelay innerComp2.Range innerComp2Body ] ceenv.builderValName ] @@ -1785,7 +1850,14 @@ let rec TryTranslateComputationExpression m1 [ implicitYieldExpr - mkSynCall "Delay" m1 [ mkSynDelay holeFill.Range holeFill ] ceenv.builderValName + // For [] builders, inject the sentinel so the Delay closure is cil managed async. + let implicitYieldDelayBody = + if builderHasRuntimeAsync ceenv then + wrapWithRuntimeAsyncSentinel holeFill + else + holeFill + + mkSynCall "Delay" m1 [ mkSynDelay holeFill.Range implicitYieldDelayBody ] ceenv.builderValName ] ceenv.builderValName @@ -2314,7 +2386,14 @@ let rec TryTranslateComputationExpression "TryWith" mTry [ - mkSynCall "Delay" mTry [ mkSynDelay2 innerExpr ] ceenv.builderValName + // For [] builders, inject the sentinel so the Delay closure is cil managed async. + let tryWithDelayBody = + if builderHasRuntimeAsync ceenv then + wrapWithRuntimeAsyncSentinel innerExpr + else + innerExpr + + mkSynCall "Delay" mTry [ mkSynDelay2 tryWithDelayBody ] ceenv.builderValName consumeExpr ] ceenv.builderValName @@ -3070,7 +3149,17 @@ let TcComputationExpression (cenv: TcFileState) env (overallTy: OverallTy) tpenv let delayedExpr = match tryFindBuilderMethod ceenv mBuilderVal "Delay" with | [] -> basicSynExpr - | _ -> mkSynCall "Delay" mDelayOrQuoteOrRun [ (mkSynDelay2 basicSynExpr) ] builderValName + | _ -> + // For [] builders, inject AsyncHelpers.Await(ValueTask.CompletedTask) as the first + // statement inside the Delay closure body. This ensures cloIsAsync = true even when the CE body + // has no let!/do! operations (which would otherwise leave the closure without any Await calls). + let delayBody = + if builderHasRuntimeAsync ceenv then + wrapWithRuntimeAsyncSentinel basicSynExpr + else + basicSynExpr + + mkSynCall "Delay" mDelayOrQuoteOrRun [ (mkSynDelay2 delayBody) ] builderValName // Add a call to 'Quote' if the method is present let quotedSynExpr = diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 27019702d75..a3a82de88d2 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -1361,6 +1361,45 @@ let private HasMethodImplNoInliningAttribute g attrs = | Some (Attrib(_, _, [ AttribInt32Arg flags ], _, _, _, _)) -> (flags &&& 0x8) <> 0x0 | _ -> false +/// Check if a method has MethodImplOptions.Async (0x2000) attribute +let private HasMethodImplAsyncAttribute g attrs = + match TryFindFSharpAttribute g g.attrib_MethodImplAttribute attrs with + // ASYNC = 0x2000 + | Some (Attrib(_, _, [ AttribInt32Arg flags ], _, _, _, _)) -> (flags &&& 0x2000) <> 0x0 + | _ -> false + +/// Check if a method has MethodImplOptions.Synchronized (0x20) attribute +let private HasMethodImplSynchronizedAttribute g attrs = + match TryFindFSharpAttribute g g.attrib_MethodImplAttribute attrs with + // SYNCHRONIZED = 0x20 + | Some (Attrib(_, _, [ AttribInt32Arg flags ], _, _, _, _)) -> (flags &&& 0x20) <> 0x0 + | _ -> false + +/// Check if type is Task, Task, ValueTask, or ValueTask +/// Used for runtime-async feature validation +let private IsTaskLikeType (g: TcGlobals) ty = + if isAppTy g ty then + let tcref = tcrefOfAppTy g ty + tyconRefEq g tcref g.system_Task_tcref || + tyconRefEq g tcref g.system_GenericTask_tcref || + tyconRefEq g tcref g.system_ValueTask_tcref || + tyconRefEq g tcref g.system_GenericValueTask_tcref + else false + +/// Unwrap Task/ValueTask to T, or Task/ValueTask to unit +/// Used for runtime-async feature: body is type-checked against T, not Task +let private UnwrapTaskLikeType (g: TcGlobals) ty = + if isAppTy g ty then + let tcref = tcrefOfAppTy g ty + let tinst = argsOfAppTy g ty + if (tyconRefEq g tcref g.system_GenericTask_tcref || tyconRefEq g tcref g.system_GenericValueTask_tcref) && tinst.Length = 1 then + tinst.[0] // Extract T from Task or ValueTask + elif tyconRefEq g tcref g.system_Task_tcref || tyconRefEq g tcref g.system_ValueTask_tcref then + g.unit_ty // Task/ValueTask -> unit + else + ty // Not a task type, return as-is + else ty + let MakeAndPublishVal (cenv: cenv) env (altActualParent, inSig, declKind, valRecInfo, vscheme, attrs, xmlDoc, konst, isGeneratedEventVal) = let g = cenv.g @@ -2403,6 +2442,20 @@ let ComputeInlineFlag (memFlagsOption: SynMemberFlags option) isInline isMutable if g.langVersion.SupportsFeature LanguageFeature.WarningWhenInliningMethodImplNoInlineMarkedFunction then warning else ignore + elif HasMethodImplAsyncAttribute g attrs then + if isInline then + // Inline runtime-async methods (e.g., RuntimeTaskBuilder.Run declared 'member inline') + // are intentionally inlined into consumer functions. The consumer function's body + // will contain AsyncHelpers.Await calls, causing the compiler to emit 'cil managed async' + // on the consumer. The consumer returns Task so the runtime wraps T→Task correctly. + ValInline.Always, ignore + else + // Non-inline runtime-async methods must never be inlined by the optimizer. + // The optimizer would inline the body (which returns T) at the call site, + // bypassing the runtime's wrapping of T -> Task. The 0x2000 flag + // tells the runtime to wrap the return value, but only when the method + // is called as a method (not inlined). + ValInline.Never, ignore elif isInline then ValInline.Always, ignore else @@ -11202,6 +11255,12 @@ and TcNormalizedBinding declKind (cenv: cenv) env tpenv overallTy safeThisValOpt let (SynValData(valInfo = valSynInfo)) = valSynData let prelimValReprInfo = TranslateSynValInfo cenv mBinding (TcAttributes cenv env) valSynInfo + // For runtime-async methods, we do NOT pre-unify overallPatTy here. + // Pre-unification would cause overallExprTy (which is the same inference variable as overallPatTy) + // to be set to unit -> Task, creating competing constraints when the body is type-checked + // against bodyExprTy = unit -> int. Instead, we post-unify overallPatTy after the body is + // type-checked and the inner cast is inserted. + // Check the pattern of the l.h.s. of the binding let tcPatPhase2, TcPatLinearEnv (tpenv, nameToPrelimValSchemeMap, _) = cenv.TcPat AllIdsOK cenv envinner (Some prelimValReprInfo) (TcPatValFlags (inlineFlag, explicitTyparInfo, argAndRetAttribs, isMutable, vis, isCompGen)) (TcPatLinearEnv (tpenv, NameMap.empty, Set.empty)) overallPatTy pat @@ -11267,9 +11326,176 @@ and TcNormalizedBinding declKind (cenv: cenv) env tpenv overallTy safeThisValOpt let envinner = { envinner with eLambdaArgInfos = argInfos; eIsControlFlow = rhsIsControlFlow } - if isCtor then TcExprThatIsCtorBody (safeThisValOpt, safeInitInfo) cenv (MustEqual overallExprTy) envinner tpenv rhsExpr - else TcExprThatCantBeCtorBody cenv (MustConvertTo (false, overallExprTy)) envinner tpenv rhsExpr - + // Validate runtime-async method constraints + // NOTE: This feature is gated by RuntimeAsync language feature in preview + // Emit a feature-gate error if the attribute is used without --langversion:preview + if HasMethodImplAsyncAttribute g valAttribs then + checkLanguageFeatureAndRecover g.langVersion LanguageFeature.RuntimeAsync mBinding + + if g.langVersion.SupportsFeature LanguageFeature.RuntimeAsync && + HasMethodImplAsyncAttribute g valAttribs then + // Check that async methods don't also have Synchronized + if HasMethodImplSynchronizedAttribute g valAttribs then + errorR(Error(FSComp.SR.tcRuntimeAsyncCannotBeSynchronized(), mBinding)) + + // For runtime-async methods, the body is type-checked against the unwrapped type T + // (not Task). The runtime handles wrapping T -> Task for the caller. + // We pre-unify overallPatTy with unit -> Task BEFORE body type-checking so the Val + // gets its correct declared type. Then we type-check the body against a FRESH + // bodyExprTy = unit -> T (completely separate from overallPatTy/overallExprTy). + // This avoids competing constraints: overallPatTy = unit -> Task and + // bodyExprTy = unit -> T are independent inference variables. + // + // IMPORTANT: mkSynBindingRhs wraps the body in SynExpr.Typed(body, Task, ...) when + // there is a return type annotation. This SynExpr.Typed wrapper would cause TcExprTypeAnnotated + // to try to unify the unwrapped type T with Task, which fails. So we must strip the + // SynExpr.Typed wrapper from the innermost lambda body before type-checking against bodyExprTy. + + // Shared helper: strip SynExpr.Typed from the innermost lambda body. + // mkSynBindingRhs adds a SynExpr.Typed wrapper when there is a return type annotation. + // Without stripping, TcExprTypeAnnotated would try to unify the unwrapped type T with + // Task (or the annotated type) and fail. + let rec stripTypedFromInnermostLambda (e: SynExpr) = + match e with + | SynExpr.Lambda(isMember, isSubsequent, spats, body, parsedData, m, trivia) -> + match body with + | SynExpr.Lambda _ -> + // Nested lambda — recurse into it + SynExpr.Lambda(isMember, isSubsequent, spats, stripTypedFromInnermostLambda body, parsedData, m, trivia) + | SynExpr.Typed(innerBody, _, _) -> + // Innermost lambda body has SynExpr.Typed wrapper — strip it + SynExpr.Lambda(isMember, isSubsequent, spats, innerBody, parsedData, m, trivia) + | _ -> + // No SynExpr.Typed wrapper — leave as-is + e + | _ -> e + + let bodyExprTy, rhsExpr = + if g.langVersion.SupportsFeature LanguageFeature.RuntimeAsync && + HasMethodImplAsyncAttribute g valAttribs && + not isInline then + match rtyOpt with + | Some (SynBindingReturnInfo(typeName = synReturnTy)) -> + // Use NewTyparsOK to resolve the return type. This allows implicitly-scoped type + // parameters (e.g., 'T in Task<'T> for a generic method like Run) to be resolved. + // With NoNewTypars, TcTypeOrMeasure throws for 'T because it is not yet in + // envinner.eNameResEnv.eTypars or tpenv (implicitly-scoped typars are not added + // to tpenv during AnalyzeAndMakeAndPublishRecursiveValue). + // With NewTyparsOK, a fresh type parameter 'T2 is created and later unified with + // the actual 'T from overallPatTy via UnifyTypes, giving the correct result. + // Use DiscardErrorsLogger to suppress any diagnostic errors from TcTypeOrMeasure. + let retTyOpt = + use _ = UseDiagnosticsLogger DiscardErrorsLogger + try Some (fst (TcTypeOrMeasure (Some TyparKind.Type) cenv NewTyparsOK CheckCxs ItemOccurrence.UseInType WarnOnIWSAM.No envinner tpenv synReturnTy)) + with RecoverableException _ -> None + match retTyOpt with + | Some retTy when IsTaskLikeType g retTy -> + let unwrappedReturnTy = UnwrapTaskLikeType g retTy + // Pre-unify overallPatTy with unit -> Task BEFORE body type-checking. + // This gives the Val its correct declared type (unit -> Task). + // IMPORTANT: overallPatTy = overallExprTy (same object, line 11137). + // Pre-unifying here sets both. bodyExprTy must share the same domain types. + // We create shared domain inference types so that after UnifyTypes unifies + // the domain types with overallPatTy's domain types, bodyTy also reflects + // those unified types. This ensures 'T in the argument and 'T in the + // return type are unified to the same type variable. + let domainTys = List.map (fun _ -> NewInferenceType g) spatsL + let fullFuncTy = List.foldBack (fun domTy acc -> mkFunTy g domTy acc) domainTys retTy + UnifyTypes cenv envinner mBinding overallPatTy fullFuncTy + // bodyTy uses the SAME domain types as fullFuncTy. + // After UnifyTypes above, domainTys are unified with overallPatTy's domain types. + // This ensures the body type-checks against the correct argument types. + let bodyTy = List.foldBack (fun domTy acc -> mkFunTy g domTy acc) domainTys unwrappedReturnTy + // Strip the SynExpr.Typed(body, Task, ...) wrapper from the innermost lambda body. + // mkSynBindingRhs adds this wrapper when there is a return type annotation. + // Without stripping, TcExprTypeAnnotated would try to unify T with Task and fail. + bodyTy, stripTypedFromInnermostLambda rhsExpr + | _ -> + // Return type is not a task-like type (e.g., 'T directly, or TcTypeOrMeasure failed). + // Fall back to using overallPatTy to extract the unwrapped return type. + // overallPatTy is already unified with the method's full type (e.g., (unit -> 'T) -> Task<'T>). + // We strip the function type to get the return type, then unwrap if task-like. + // Strip the SynExpr.Typed wrapper to avoid type mismatch from the return type annotation. + // Strip the function type from overallPatTy to get the return type. + // Then unwrap if task-like (e.g., Task<'T> -> 'T). Build body type as unit -> 'T. + let _, declaredRetTy = stripFunTy g overallPatTy + let unwrappedRetTy = UnwrapTaskLikeType g declaredRetTy + let bodyTy = List.foldBack (fun _ acc -> mkFunTy g (NewInferenceType g) acc) spatsL unwrappedRetTy + bodyTy, stripTypedFromInnermostLambda rhsExpr + | None -> overallExprTy, rhsExpr + elif g.langVersion.SupportsFeature LanguageFeature.RuntimeAsync && + isInline && + Option.isSome memberFlagsOpt && + (match env.eFamilyType with + | Some tcref -> TryFindFSharpAttribute g g.attrib_RuntimeAsyncAttribute tcref.Attribs |> Option.isSome + | None -> false) then + // For inline members of []-attributed types, allow the lambda body to + // return 'T where Task<'T> is expected. This enables Delay to be written as: + // member inline _.Delay(f: unit -> 'T) : unit -> Task<'T> = fun () -> f() + // without needing cast<'T, Task<'T>>(f()). + // + // The return type annotation may be a function type returning a task (e.g., unit -> Task<'T>). + // We strip the function type to get the innermost return type, check if it is task-like, + // unwrap it, and reconstruct the function type with the unwrapped return type. + // This gives bodyExprTy = (domain) -> (unit -> 'T) instead of (domain) -> (unit -> Task<'T>). + match rtyOpt with + | Some (SynBindingReturnInfo(typeName = synReturnTy)) -> + // Use NewTyparsOK to resolve the return type. This allows implicitly-scoped type + // parameters (e.g., 'T in unit -> Task<'T>) to be resolved. + // Use DiscardErrorsLogger to suppress any diagnostic errors from TcTypeOrMeasure. + let retTyOpt = + use _ = UseDiagnosticsLogger DiscardErrorsLogger + try Some (fst (TcTypeOrMeasure (Some TyparKind.Type) cenv NewTyparsOK CheckCxs ItemOccurrence.UseInType WarnOnIWSAM.No envinner tpenv synReturnTy)) + with RecoverableException _ -> None + match retTyOpt with + | Some retTy -> + // Strip function types to get the innermost return type. + // For unit -> Task<'T>, this gives innerDomainTys=[unit], innerRetTy=Task<'T>. + // For Task<'T> directly, this gives innerDomainTys=[], innerRetTy=Task<'T>. + let innerDomainTys, innerRetTy = stripFunTy g retTy + if IsTaskLikeType g innerRetTy then + let unwrappedInnerRetTy = UnwrapTaskLikeType g innerRetTy + // Reconstruct the unwrapped return type (e.g., unit -> 'T from unit -> Task<'T>) + let unwrappedRetTy = List.foldBack (fun domTy acc -> mkFunTy g domTy acc) innerDomainTys unwrappedInnerRetTy + // Pre-unify overallPatTy with the full function type using the original return type. + // This gives the Val its correct declared type (e.g., (unit -> 'T) -> (unit -> Task<'T>)). + // We create shared domain inference types so that after UnifyTypes unifies + // the domain types with overallPatTy's domain types, bodyTy also reflects + // those unified types. + let domainTys = List.map (fun _ -> NewInferenceType g) spatsL + let fullFuncTy = List.foldBack (fun domTy acc -> mkFunTy g domTy acc) domainTys retTy + UnifyTypes cenv envinner mBinding overallPatTy fullFuncTy + // bodyTy uses the SAME domain types as fullFuncTy but with the unwrapped return type. + // After UnifyTypes above, domainTys are unified with overallPatTy's domain types. + let bodyTy = List.foldBack (fun domTy acc -> mkFunTy g domTy acc) domainTys unwrappedRetTy + // Strip the SynExpr.Typed(body, unit -> Task, ...) wrapper from the innermost + // lambda body if present. This is a safety measure for cases where mkSynBindingRhs + // adds a SynExpr.Typed wrapper around the body. + bodyTy, stripTypedFromInnermostLambda rhsExpr + else + overallExprTy, rhsExpr + | None -> overallExprTy, rhsExpr + | None -> overallExprTy, rhsExpr + else + overallExprTy, rhsExpr + + let rhsExprChecked, tpenv = + if isCtor then TcExprThatIsCtorBody (safeThisValOpt, safeInitInfo) cenv (MustEqual overallExprTy) envinner tpenv rhsExpr + else TcExprThatCantBeCtorBody cenv (MustConvertTo (false, bodyExprTy)) envinner tpenv rhsExpr + + rhsExprChecked, tpenv + + // Return type validation AFTER type inference (overallPatTy is now resolved) + if g.langVersion.SupportsFeature LanguageFeature.RuntimeAsync && + HasMethodImplAsyncAttribute g valAttribs && + not isInline then + let _, returnTy = stripFunTy g overallPatTy + // Check that async methods return Task, Task, ValueTask, or ValueTask + if not (IsTaskLikeType g returnTy) then + errorR(Error(FSComp.SR.tcRuntimeAsyncMethodMustReturnTask(NicePrint.minimalStringOfType env.DisplayEnv returnTy), mBinding)) + // Check that async methods don't return byref types + if isByrefTy g returnTy then + errorR(Error(FSComp.SR.tcRuntimeAsyncCannotReturnByref(), mBinding)) if kind = SynBindingKind.StandaloneExpression && not cenv.isScript then UnifyUnitType cenv env mBinding overallPatTy rhsExprChecked |> ignore diff --git a/src/Compiler/Checking/InfoReader.fs b/src/Compiler/Checking/InfoReader.fs index 0e564c3add8..e2e4bae8f28 100644 --- a/src/Compiler/Checking/InfoReader.fs +++ b/src/Compiler/Checking/InfoReader.fs @@ -857,6 +857,9 @@ type InfoReader(g: TcGlobals, amap: ImportMap) as this = let isRuntimeFeatureVirtualStaticsInInterfacesSupported = lazy isRuntimeFeatureSupported "VirtualStaticsInInterfaces" + let isRuntimeFeatureAsyncSupported = + lazy isRuntimeFeatureSupported "Async" + member _.g = g member _.amap = amap @@ -921,6 +924,7 @@ type InfoReader(g: TcGlobals, amap: ImportMap) as this = // Both default and static interface method consumption features are tied to the runtime support of DIMs. | LanguageFeature.DefaultInterfaceMemberConsumption -> isRuntimeFeatureDefaultImplementationsOfInterfacesSupported.Value | LanguageFeature.InterfacesWithAbstractStaticMembers -> isRuntimeFeatureVirtualStaticsInInterfacesSupported.Value + | LanguageFeature.RuntimeAsync -> isRuntimeFeatureAsyncSupported.Value | _ -> true /// Get the declared constructors of any F# type diff --git a/src/Compiler/Checking/PostInferenceChecks.fs b/src/Compiler/Checking/PostInferenceChecks.fs index 16b29b71ab6..846ff1ccbdd 100644 --- a/src/Compiler/Checking/PostInferenceChecks.fs +++ b/src/Compiler/Checking/PostInferenceChecks.fs @@ -1837,7 +1837,15 @@ and CheckLambdas isTop (memberVal: Val option) cenv env inlined valReprInfo alwa // Check argument types for arg in syntacticArgs do - if arg.InlineIfLambda && (not inlined || not (isFunTy g arg.Type || isFSharpDelegateTy g arg.Type)) then + // Allow [] on parameters of runtime-async methods (MethodImplOptions.Async = 0x2000). + // These methods are declared 'inline' but compiled as real methods (ValInline.Never) due to the + // async attribute. Their lambda parameters are still inlined into the method body at the call site. + let isRuntimeAsyncMember = + memberVal |> Option.exists (fun v -> + match TryFindFSharpAttribute g g.attrib_MethodImplAttribute v.Attribs with + | Some (Attrib(_, _, [ AttribInt32Arg flags ], _, _, _, _)) -> (flags &&& 0x2000) <> 0x0 + | _ -> false) + if arg.InlineIfLambda && ((not inlined && not isRuntimeAsyncMember) || not (isFunTy g arg.Type || isFSharpDelegateTy g arg.Type)) then errorR(Error(FSComp.SR.tcInlineIfLambdaUsedOnNonInlineFunctionOrMethod(), arg.Range)) CheckValSpecAux permitByRefType cenv env arg (fun () -> diff --git a/src/Compiler/CodeGen/EraseClosures.fs b/src/Compiler/CodeGen/EraseClosures.fs index 6585fa1d661..eb5c7c76bf6 100644 --- a/src/Compiler/CodeGen/EraseClosures.fs +++ b/src/Compiler/CodeGen/EraseClosures.fs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. module internal FSharp.Compiler.AbstractIL.ILX.EraseClosures @@ -181,6 +181,7 @@ let rec mkTyOfLambdas cenv lam = | Lambdas_lambda(d, r) -> mkILFuncTy cenv d.Type (mkTyOfLambdas cenv r) | Lambdas_forall _ -> cenv.mkILTyFuncTy + // -------------------------------------------------------------------- // Method to call for a particular multi-application // -------------------------------------------------------------------- @@ -682,13 +683,14 @@ let rec convIlxClosureDef cenv encl (td: ILTypeDef) clo = let convil = convILMethodBody (Some nowCloSpec, None) clo.cloCode.Value let nowApplyMethDef = - mkILNonGenericVirtualInstanceMethod ( + (mkILNonGenericVirtualInstanceMethod ( "Invoke", ILMemberAccess.Public, nowParams, mkILReturn nowReturnTy, MethodBody.IL(notlazy convil) - ) + )) + .WithAsync(clo.cloIsAsync) let ctorMethodDef = mkILStorageCtor ( diff --git a/src/Compiler/CodeGen/IlxGen.fs b/src/Compiler/CodeGen/IlxGen.fs index 290bba23e4d..6171fc4cdab 100644 --- a/src/Compiler/CodeGen/IlxGen.fs +++ b/src/Compiler/CodeGen/IlxGen.fs @@ -6693,6 +6693,17 @@ and GenClosureTypeDefs ) = let g = cenv.g + // Returns true if the IL method body contains a call to AsyncHelpers.Await, AwaitAwaiter, or UnsafeAwaitAwaiter. + // Used to determine whether a closure's Invoke method should be emitted as 'cil managed async'. + let ilBodyContainsAsyncHelpersAwait (body: ILMethodBody) = + body.Code.Instrs |> Array.exists (fun instr -> + match instr with + | I_call(_, mspec, _) | I_callvirt(_, mspec, _) -> + mspec.MethodRef.DeclaringTypeRef.FullName = "System.Runtime.CompilerServices.AsyncHelpers" + && (mspec.MethodRef.Name = "Await" || mspec.MethodRef.Name = "AwaitAwaiter" || mspec.MethodRef.Name = "UnsafeAwaitAwaiter") + | _ -> false + ) + let cloInfo = { cloFreeVars = ilCloAllFreeVars @@ -6702,6 +6713,10 @@ and GenClosureTypeDefs (match cloSpec with | None -> false | Some cloSpec -> cloSpec.UseStaticField) + // Set cloIsAsync = true if the closure body contains AsyncHelpers.Await calls. + // This causes EraseClosures.fs to emit the Invoke method as 'cil managed async', + // which is required for AsyncHelpers.Await to work correctly at runtime. + cloIsAsync = ilBodyContainsAsyncHelpersAwait ilCtorBody } let mdefs, fdefs = @@ -7080,9 +7095,37 @@ and GetIlxClosureFreeVars cenv m (thisVars: ValRef list) boxity eenv takenNames and GetIlxClosureInfo cenv m boxity isLocalTypeFunc canUseStaticField thisVars eenvouter expr = let g = cenv.g + // Save the declared return type before getCallStructure can override it. + // When the closure body type differs from the declared function return type and the + // declared return is Task-like, we must use the declared type for Lambdas_return. + // This is needed when a type-checking coercion strips the Task<'T> wrapper from the + // body expression (e.g. after MethodImpl(0x2000) async coercion), leaving bodyRetTy = 'T + // while the closure's overall F# type is still unit -> Task<'T>. + // Using bodyRetTy ('T) for Lambdas_return would cause an IL mismatch: the closure class + // inherits FSharpFunc> whose Invoke signature says Task<'T>, but the + // generated Invoke method would declare 'T, causing a TypeLoadException at runtime. + // NOTE: getCallStructure ignores the ety parameter for Expr.Lambda (uses bty instead), + // so any Task-like correction made to returnTy before getCallStructure is discarded. + // We save it here and restore it after getCallStructure. + let declaredRetTyOpt = + match expr with + | Expr.Lambda(_, _, _, _, _, _, bodyRetTy) -> + let exprTy = tyOfExpr g expr + let _, declaredRetTy = stripFunTy g exprTy + if isAppTy g declaredRetTy + && (tyconRefEq g (tcrefOfAppTy g declaredRetTy) g.system_Task_tcref + || tyconRefEq g (tcrefOfAppTy g declaredRetTy) g.system_GenericTask_tcref + || tyconRefEq g (tcrefOfAppTy g declaredRetTy) g.system_ValueTask_tcref + || tyconRefEq g (tcrefOfAppTy g declaredRetTy) g.system_GenericValueTask_tcref) + && not (typeEquiv g bodyRetTy declaredRetTy) then + Some declaredRetTy + else + None + | _ -> None + let returnTy = match expr with - | Expr.Lambda(_, _, _, _, _, _, returnTy) + | Expr.Lambda(_, _, _, _, _, _, bodyRetTy) -> bodyRetTy | Expr.TyLambda(_, _, _, _, returnTy) -> returnTy | _ -> tyOfExpr g expr @@ -7101,6 +7144,39 @@ and GetIlxClosureInfo cenv m boxity isLocalTypeFunc canUseStaticField thisVars e getCallStructure [] [] (expr, returnTy) + // After getCallStructure, restore the declared Task-like return type if we saved one. + // getCallStructure ignores the ety parameter for Expr.Lambda (it uses bty from the lambda + // node instead), so the Task-like correction from declaredRetTyOpt would be lost otherwise. + // Fall back to the ExprContainsAsyncHelpersAwaitCall check for cases not covered by + // declaredRetTyOpt (e.g. when the sentinel is present in the typed tree body). + let returnTy = + match declaredRetTyOpt with + | Some declaredRetTy when not (typeEquiv g returnTy declaredRetTy) -> + // Use the declared Task-like return type instead of the body type + declaredRetTy + | _ -> + // When a Delay closure for a [] builder is inlined, its F# return type becomes + // 'T rather than Task<'T>. This is because the [] Delay body 'fun () -> f()' + // inlines to 'fun () -> ' whose body type is 'T (not Task<'T>). After inlining, + // getCallStructure strips the lambda and returns returnTy = 'T from the innermost body. + // However, CE desugaring auto-injects AsyncHelpers.Await(ValueTask.CompletedTask) (the sentinel) + // into every Delay body for [] builders. This causes cloIsAsync = true (detected + // at IL level by ilBodyContainsAsyncHelpersAwait), which causes EraseClosures to emit the + // Invoke method as 'cil managed async'. A 'cil managed async' method with return type 'T + // (where 'T is not Task-like) is invalid at runtime and causes TypeLoadException ("format is invalid"). + // Fix: if the body contains AsyncHelpers.Await calls and returnTy is not already Task-like, + // wrap returnTy in Task so the closure class inherits FSharpFunc> + // and Invoke declares 'Task cil managed async', which is valid. + if ExprContainsAsyncHelpersAwaitCall body + && not (isAppTy g returnTy + && (tyconRefEq g (tcrefOfAppTy g returnTy) g.system_Task_tcref + || tyconRefEq g (tcrefOfAppTy g returnTy) g.system_GenericTask_tcref + || tyconRefEq g (tcrefOfAppTy g returnTy) g.system_ValueTask_tcref + || tyconRefEq g (tcrefOfAppTy g returnTy) g.system_GenericValueTask_tcref)) then + mkWoNullAppTy g.system_GenericTask_tcref [returnTy] + else + returnTy + let takenNames = vs |> List.map (fun v -> v.CompiledName g.CompilerGlobalState) // Get the free variables and the information about the closure, add the free variables to the environment @@ -9181,7 +9257,64 @@ and ComputeMethodImplAttribs cenv (_v: Val) attrs = let hasSynchronizedImplFlag = (implflags &&& 0x20) <> 0x0 let hasNoInliningImplFlag = (implflags &&& 0x08) <> 0x0 let hasAggressiveInliningImplFlag = (implflags &&& 0x0100) <> 0x0 - hasPreserveSigImplFlag, hasSynchronizedImplFlag, hasNoInliningImplFlag, hasAggressiveInliningImplFlag, attrs + let hasAsyncImplFlag = (implflags &&& 0x2000) <> 0x0 + hasPreserveSigImplFlag, hasSynchronizedImplFlag, hasNoInliningImplFlag, hasAggressiveInliningImplFlag, hasAsyncImplFlag, attrs + +/// Check if an expression contains calls to System.Runtime.CompilerServices.AsyncHelpers.Await/AwaitAwaiter/UnsafeAwaitAwaiter. +/// This is used to detect when async code has been inlined into a method, which means the +/// containing method should also have the async flag set. +and ExprContainsAsyncHelpersAwaitCall expr = + let rec check expr = + match expr with + | Expr.Op (TOp.ILCall (_, _, _, _, _, _, _, ilMethRef, _, _, _), _, args, _) -> + // Check if this is a call to AsyncHelpers.Await, AwaitAwaiter, or UnsafeAwaitAwaiter + if ilMethRef.DeclaringTypeRef.FullName = "System.Runtime.CompilerServices.AsyncHelpers" + && (ilMethRef.Name = "Await" || ilMethRef.Name = "AwaitAwaiter" || ilMethRef.Name = "UnsafeAwaitAwaiter") then + true + else + args |> List.exists check + | Expr.Op (_, _, args, _) -> + args |> List.exists check + | Expr.Let (bind, body, _, _) -> + check bind.Expr || check body + | Expr.LetRec (binds, body, _, _) -> + binds |> List.exists (fun b -> check b.Expr) || check body + | Expr.Sequential (e1, e2, _, _) -> + check e1 || check e2 + | Expr.Lambda _ -> + false // Lambda bodies become separate closure classes; don't propagate async flag across lambda boundaries + | Expr.TyLambda (_, _, body, _, _) -> + check body + | Expr.App (f, _, _, args, _) -> + check f || args |> List.exists check + | Expr.Match (_, _, dtree, targets, _, _) -> + checkDecisionTree dtree || targets |> Array.exists (fun (TTarget(_, e, _)) -> check e) + | Expr.TyChoose (_, body, _) -> + check body + | Expr.Link eref -> + check eref.Value + | Expr.DebugPoint (_, innerExpr) -> + check innerExpr + | Expr.Obj (_, _, _, basecall, overrides, iimpls, _) -> + check basecall + || overrides |> List.exists (fun (TObjExprMethod(_, _, _, _, e, _)) -> check e) + || iimpls |> List.exists (fun (_, overrides) -> overrides |> List.exists (fun (TObjExprMethod(_, _, _, _, e, _)) -> check e)) + | Expr.StaticOptimization (_, e1, e2, _) -> + check e1 || check e2 + | Expr.Quote _ + | Expr.Const _ + | Expr.Val _ + | Expr.WitnessArg _ -> + false + and checkDecisionTree dtree = + match dtree with + | TDSuccess (args, _) -> args |> List.exists check + | TDSwitch (e, cases, dflt, _) -> + check e || cases |> List.exists (fun (TCase(_, t)) -> checkDecisionTree t) || dflt |> Option.exists checkDecisionTree + | TDBind (bind, rest) -> + check bind.Expr || checkDecisionTree rest + check expr + and GenMethodForBinding cenv @@ -9296,10 +9429,42 @@ and GenMethodForBinding )) ] - // Discard the result on a 'void' return type. For a constructor just return 'void' + // Discard the result on a 'void' return type. For a constructor just return 'void'. + // For runtime-async methods returning Task or ValueTask (non-generic), the spec says the stack + // should be empty before 'ret'. The body returns unit (nothing on stack), so we use + // discardAndReturnVoid to discard the unit value and emit 'ret' with empty stack. + // + // NOTE: This early computation of the 0x2000 MethodImpl flag duplicates what + // ComputeMethodImplAttribs (called at line ~9539 as hasAsyncImplFlagFromAttr) also computes. + // The duplication is structurally necessary: hasAsyncImplFlagEarly is needed here to configure + // eenvForMeth (withinSEH=true, to suppress tail calls) and sequel (discardAndReturnVoid for + // non-generic Task/ValueTask), both of which must be established before the method body is + // code-generated. ComputeMethodImplAttribs is only called after eenvForMeth and sequel are + // already in use, so the flag cannot be deferred to that call. + let hasAsyncImplFlagEarly = + match TryFindFSharpAttribute g g.attrib_MethodImplAttribute v.Attribs with + | Some(Attrib(_, _, [ AttribInt32Arg flags ], _, _, _, _)) -> (flags &&& 0x2000) <> 0x0 + | _ -> false + // For 'cil managed async' methods (those with []), suppress tail calls + // in the method body. This is required because 'cil managed async' methods rely on their stack + // frame remaining alive so the runtime can suspend and resume them when AsyncHelpers.Await is + // called on a non-completed task. A 'tail.' prefix eliminates the frame before the callee runs, + // preventing suspension. Setting withinSEH=true reuses the existing tail-call suppression + // mechanism without affecting self-recursive branch calls (which use a separate code path). + let eenvForMeth = + if hasAsyncImplFlagEarly then { eenvForMeth with withinSEH = true } + else eenvForMeth + let isNonGenericTaskOrValueTask = + isAppTy g returnTy && + (tyconRefEq g (tcrefOfAppTy g returnTy) g.system_Task_tcref || + tyconRefEq g (tcrefOfAppTy g returnTy) g.system_ValueTask_tcref) let sequel = if isUnitTy g returnTy then discardAndReturnVoid elif isCtor then ReturnVoid + elif hasAsyncImplFlagEarly && isNonGenericTaskOrValueTask then + // Runtime-async methods returning Task or ValueTask (non-generic) must have an empty + // stack before 'ret'. The body returns unit, so we discard the unit value. + discardAndReturnVoid else Return // Now generate the code. @@ -9317,15 +9482,29 @@ and GenMethodForBinding | Some(Attrib(_, _, _, _, _, _, m)) -> error (Error(FSComp.SR.ilDllImportAttributeCouldNotBeDecoded (), m)) | _ -> - // Replace the body of ValInline.PseudoVal "must inline" methods with a 'throw' + // Replace the body of ValInline.Always "must inline" methods with a 'throw' // For witness-passing methods, don't do this if `isLegacy` flag specified // on the attribute. Older compilers let bodyExpr = + // Check if the declaring type has [] - if so, implicitly apply NoDynamicInvocation + // to all members of that type, so library authors don't need to annotate every CE member. + // Extension methods in separate modules are not affected (they have a different declaring type). + // Only apply to must-inline methods (ValInline.Always). Non-inline methods (ValInline.Never), + // such as Run with [], must not have their bodies replaced — they need + // to execute the CE body (f()). + let hasDeclTypeRuntimeAsync = + v.InlineInfo = ValInline.Always && + match v.MemberInfo with + | Some memberInfo -> + TryFindFSharpAttribute cenv.g cenv.g.attrib_RuntimeAsyncAttribute memberInfo.ApparentEnclosingEntity.Attribs + |> Option.isSome + | None -> false + let attr = TryFindFSharpBoolAttributeAssumeFalse cenv.g cenv.g.attrib_NoDynamicInvocationAttribute v.Attribs if - (not generateWitnessArgs && attr.IsSome) + (not generateWitnessArgs && (attr.IsSome || hasDeclTypeRuntimeAsync)) || (generateWitnessArgs && attr = Some false) then let exnArg = @@ -9365,9 +9544,55 @@ and GenMethodForBinding | _ -> [], None // check if the hasPreserveSigNamedArg and hasSynchronizedImplFlag implementation flags have been specified - let hasPreserveSigImplFlag, hasSynchronizedImplFlag, hasNoInliningFlag, hasAggressiveInliningImplFlag, attrs = + let hasPreserveSigImplFlag, hasSynchronizedImplFlag, hasNoInliningFlag, hasAggressiveInliningImplFlag, hasAsyncImplFlagFromAttr, attrs = ComputeMethodImplAttribs cenv v attrs + // Check if the method body contains calls to AsyncHelpers.Await - if so, the method should also have the async flag. + // This handles the case where an inline async method's Run is inlined into a consumer function, + // causing the consumer's body to contain AsyncHelpers.Await calls directly. + // Guards: + // 1. [] methods have their body replaced with 'throw', so we must not + // propagate the async flag from the original body. This also covers members of RuntimeAsync- + // marked builder types (they get implicit NoDynamicInvocation), so builder members like Bind + // do not get 'cil managed async' from body analysis. + // 2. Only methods returning Task-like types (Task, Task, ValueTask, ValueTask) can be + // 'cil managed async'. If the optimizer inlines an async function into a non-Task-returning + // method (e.g. main : int), we must NOT set the flag or the runtime will reject it. + // 3. The explicit [] path requires [] on the enclosing entity. + // The body-analysis path does NOT require it, so that consumer functions in modules + // without [] (e.g. Api.consumeOlderTaskCE) are correctly marked when the + // inline Run body is inlined into them. + let hasAsyncImplFlag = + let hasNoDynamicInvocation = + (TryFindFSharpBoolAttributeAssumeFalse g g.attrib_NoDynamicInvocationAttribute v.Attribs + |> Option.isSome) + || (match v.MemberInfo with + | Some memberInfo -> + TryFindFSharpAttribute g g.attrib_RuntimeAsyncAttribute memberInfo.ApparentEnclosingEntity.Attribs + |> Option.isSome + | None -> false) + let returnsTaskLikeType = + isAppTy g returnTy && + (tyconRefEq g (tcrefOfAppTy g returnTy) g.system_Task_tcref || + tyconRefEq g (tcrefOfAppTy g returnTy) g.system_GenericTask_tcref || + tyconRefEq g (tcrefOfAppTy g returnTy) g.system_ValueTask_tcref || + tyconRefEq g (tcrefOfAppTy g returnTy) g.system_GenericValueTask_tcref) + // Check if the enclosing entity has [] for the explicit 0x2000 path. + // This ensures that only methods in RuntimeAsync-marked types get 'cil managed async' + // from an explicit [] attribute. + let enclosingEntityHasRuntimeAsync = + match v.TryDeclaringEntity with + | Parent entityRef -> + TryFindFSharpAttribute g g.attrib_RuntimeAsyncAttribute entityRef.Attribs + |> Option.isSome + | ParentNone -> false + // Explicit [] on a method requires [] on the enclosing entity. + // Body analysis (detecting inlined AsyncHelpers.Await calls) does NOT require [] + // on the enclosing entity — consumer functions in plain modules get 'cil managed async' when + // the inline Run body is inlined into them. + (hasAsyncImplFlagFromAttr && enclosingEntityHasRuntimeAsync) + || (not hasNoDynamicInvocation && returnsTaskLikeType && ExprContainsAsyncHelpersAwaitCall body) + let securityAttributes, attrs = attrs |> List.partition (fun a -> IsSecurityAttribute g cenv.amap cenv.casApplied a m) @@ -9640,6 +9865,7 @@ and GenMethodForBinding .WithSynchronized(hasSynchronizedImplFlag) .WithNoInlining(hasNoInliningFlag) .WithAggressiveInlining(hasAggressiveInliningImplFlag) + .WithAsync(hasAsyncImplFlag) .With(isEntryPoint = isExplicitEntryPoint, securityDecls = secDecls) let mdef = @@ -10707,7 +10933,7 @@ and GenAbstractBinding cenv eenv tref (vref: ValRef) = let memberInfo = Option.get vref.MemberInfo let attribs = vref.Attribs - let hasPreserveSigImplFlag, hasSynchronizedImplFlag, hasNoInliningFlag, hasAggressiveInliningImplFlag, attribs = + let hasPreserveSigImplFlag, hasSynchronizedImplFlag, hasNoInliningFlag, hasAggressiveInliningImplFlag, hasAsyncImplFlag, attribs = ComputeMethodImplAttribs cenv vref.Deref attribs if memberInfo.MemberFlags.IsDispatchSlot && not memberInfo.IsImplemented then @@ -10761,6 +10987,7 @@ and GenAbstractBinding cenv eenv tref (vref: ValRef) = .WithSynchronized(hasSynchronizedImplFlag) .WithNoInlining(hasNoInliningFlag) .WithAggressiveInlining(hasAggressiveInliningImplFlag) + .WithAsync(hasAsyncImplFlag) match memberInfo.MemberFlags.MemberKind with | SynMemberKind.ClassConstructor diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index d24c8aba0eb..80d5b52b482 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1808,6 +1808,11 @@ featureReturnFromFinal,"Support for ReturnFromFinal/YieldFromFinal in computatio featureMethodOverloadsCache,"Support for caching method overload resolution results for improved compilation performance." featureImplicitDIMCoverage,"Implicit dispatch slot coverage for default interface member implementations" featurePreprocessorElif,"#elif preprocessor directive" +featureRuntimeAsync,"runtime async" +3884,tcRuntimeAsyncMethodMustReturnTask,"Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '%s'." +3885,tcRuntimeAsyncCannotBeSynchronized,"Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized." +3886,tcRuntimeAsyncCannotReturnByref,"Methods marked with MethodImplOptions.Async cannot return byref types." +3887,tcRuntimeAsyncNotSupported,"Methods marked with MethodImplOptions.Async are not supported in this context." 3880,optsLangVersionOutOfSupport,"Language version '%s' is out of support. The last .NET SDK supporting it is available at https://dotnet.microsoft.com/en-us/download/dotnet/%s" 3881,optsUnrecognizedLanguageFeature,"Unrecognized language feature name: '%s'. Use a valid feature name such as 'NameOf' or 'StringInterpolation'." 3882,lexHashElifMustBeFirst,"#elif directive must appear as the first non-whitespace character on a line" diff --git a/src/Compiler/Facilities/LanguageFeatures.fs b/src/Compiler/Facilities/LanguageFeatures.fs index 0303881b08d..600ad97c93c 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fs +++ b/src/Compiler/Facilities/LanguageFeatures.fs @@ -107,6 +107,7 @@ type LanguageFeature = | MethodOverloadsCache | ImplicitDIMCoverage | PreprocessorElif + | RuntimeAsync /// LanguageVersion management type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) = @@ -257,6 +258,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) LanguageFeature.FromEndSlicing, previewVersion // Unfinished features --- needs work LanguageFeature.MethodOverloadsCache, previewVersion // Performance optimization for overload resolution LanguageFeature.ImplicitDIMCoverage, languageVersion110 + LanguageFeature.RuntimeAsync, previewVersion // Runtime-async support for .NET 10+ ] static let defaultLanguageVersion = LanguageVersion("default") @@ -449,6 +451,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) | LanguageFeature.MethodOverloadsCache -> FSComp.SR.featureMethodOverloadsCache () | LanguageFeature.ImplicitDIMCoverage -> FSComp.SR.featureImplicitDIMCoverage () | LanguageFeature.PreprocessorElif -> FSComp.SR.featurePreprocessorElif () + | LanguageFeature.RuntimeAsync -> FSComp.SR.featureRuntimeAsync () /// Get a version string associated with the given feature. static member GetFeatureVersionString feature = diff --git a/src/Compiler/Facilities/LanguageFeatures.fsi b/src/Compiler/Facilities/LanguageFeatures.fsi index 20ea1175ec8..a5c62a5ac10 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fsi +++ b/src/Compiler/Facilities/LanguageFeatures.fsi @@ -98,6 +98,7 @@ type LanguageFeature = | MethodOverloadsCache | ImplicitDIMCoverage | PreprocessorElif + | RuntimeAsync /// LanguageVersion management type LanguageVersion = diff --git a/src/Compiler/Optimize/Optimizer.fs b/src/Compiler/Optimize/Optimizer.fs index 0eba72d17ff..16627c92e6f 100644 --- a/src/Compiler/Optimize/Optimizer.fs +++ b/src/Compiler/Optimize/Optimizer.fs @@ -2334,6 +2334,56 @@ let inline IsStateMachineExpr g overallExpr = isReturnsResumableCodeTy g valRef.TauType | _ -> false +/// Check if an expression tree contains calls to System.Runtime.CompilerServices.AsyncHelpers.Await/AwaitAwaiter/UnsafeAwaitAwaiter. +/// Functions containing these calls use 'cil managed async' and their bodies rely on the runtime +/// wrapping the return value into Task. Such functions must not be inlined into non-async callers +/// because the unsafe cast (T -> Task) only works with runtime-async wrapping. +let rec private exprContainsAsyncHelpersAwait expr = + match expr with + | Expr.Op (TOp.ILCall (_, _, _, _, _, _, _, ilMethRef, _, _, _), _, args, _) -> + (ilMethRef.DeclaringTypeRef.FullName = "System.Runtime.CompilerServices.AsyncHelpers" + && (ilMethRef.Name = "Await" || ilMethRef.Name = "AwaitAwaiter" || ilMethRef.Name = "UnsafeAwaitAwaiter")) + || args |> List.exists exprContainsAsyncHelpersAwait + | Expr.Op (_, _, args, _) -> + args |> List.exists exprContainsAsyncHelpersAwait + | Expr.Let (bind, body, _, _) -> + exprContainsAsyncHelpersAwait bind.Expr || exprContainsAsyncHelpersAwait body + | Expr.LetRec (binds, body, _, _) -> + binds |> List.exists (fun b -> exprContainsAsyncHelpersAwait b.Expr) || exprContainsAsyncHelpersAwait body + | Expr.Sequential (e1, e2, _, _) -> + exprContainsAsyncHelpersAwait e1 || exprContainsAsyncHelpersAwait e2 + | Expr.Lambda (_, _, _, _, body, _, _) -> exprContainsAsyncHelpersAwait body // Walk through lambda bodies (optimization data stores full lambda expression) + | Expr.TyLambda (_, _, body, _, _) -> exprContainsAsyncHelpersAwait body + | Expr.App (f, _, _, args, _) -> + exprContainsAsyncHelpersAwait f || args |> List.exists exprContainsAsyncHelpersAwait + | Expr.Match (_, _, dtree, targets, _, _) -> + exprContainsAsyncHelpersAwaitDTree dtree + || targets |> Array.exists (fun (TTarget(_, e, _)) -> exprContainsAsyncHelpersAwait e) + | Expr.TyChoose (_, body, _) -> exprContainsAsyncHelpersAwait body + | Expr.Link eref -> exprContainsAsyncHelpersAwait eref.Value + | Expr.DebugPoint (_, innerExpr) -> exprContainsAsyncHelpersAwait innerExpr + | Expr.Obj (_, _, _, basecall, overrides, iimpls, _) -> + exprContainsAsyncHelpersAwait basecall + || overrides |> List.exists (fun (TObjExprMethod(_, _, _, _, e, _)) -> exprContainsAsyncHelpersAwait e) + || iimpls |> List.exists (fun (_, overrides) -> overrides |> List.exists (fun (TObjExprMethod(_, _, _, _, e, _)) -> exprContainsAsyncHelpersAwait e)) + | Expr.StaticOptimization (_, e1, e2, _) -> + exprContainsAsyncHelpersAwait e1 || exprContainsAsyncHelpersAwait e2 + | Expr.Quote _ + | Expr.Const _ + | Expr.Val _ + | Expr.WitnessArg _ -> + false + +and private exprContainsAsyncHelpersAwaitDTree dtree = + match dtree with + | TDSuccess (args, _) -> args |> List.exists exprContainsAsyncHelpersAwait + | TDSwitch (e, cases, dflt, _) -> + exprContainsAsyncHelpersAwait e + || cases |> List.exists (fun (TCase(_, t)) -> exprContainsAsyncHelpersAwaitDTree t) + || dflt |> Option.exists exprContainsAsyncHelpersAwaitDTree + | TDBind (bind, rest) -> + exprContainsAsyncHelpersAwait bind.Expr || exprContainsAsyncHelpersAwaitDTree rest + /// Optimize/analyze an expression let rec OptimizeExpr cenv (env: IncrementalOptimizationEnv) expr = cenv.stackGuard.Guard <| fun () -> @@ -4154,6 +4204,14 @@ and OptimizeBinding cenv isRec env (TBind(vref, expr, spBind)) = elif fvs.FreeLocals.ToArray() |> Seq.fold(fun acc v -> if not acc then v.Accessibility.IsPrivate else acc) false then // Discarding lambda for binding because uses private members UnknownValue + elif exprContainsAsyncHelpersAwait body then + // Discarding lambda for binding because contains AsyncHelpers.Await calls. + // Any function whose body contains AsyncHelpers.Await/AwaitAwaiter/UnsafeAwaitAwaiter + // calls must not be cross-module inlined by the optimizer. These calls only work + // correctly within a 'cil managed async' context. If such a function were inlined + // into a non-async caller (e.g., main), the runtime would produce garbage results. + // This applies to ALL functions with Await calls, not just those in RuntimeAsync-marked types. + UnknownValue else ivalue @@ -4164,7 +4222,9 @@ and OptimizeBinding cenv isRec env (TBind(vref, expr, spBind)) = | UnknownValue | ConstValue _ | ConstExprValue _ -> ivalue | SizeValue(_, a) -> MakeSizedValueInfo (cut a) - let einfo = if vref.ShouldInline || vref.InlineIfLambda then einfo else {einfo with Info = cut einfo.Info } + let einfo = + if vref.ShouldInline || vref.InlineIfLambda then einfo + else {einfo with Info = cut einfo.Info } let einfo = if (not vref.ShouldInline && not vref.InlineIfLambda && not cenv.settings.KeepOptimizationValues) || diff --git a/src/Compiler/TypedTree/TcGlobals.fs b/src/Compiler/TypedTree/TcGlobals.fs index 3e0bdfd0905..83a02487be6 100644 --- a/src/Compiler/TypedTree/TcGlobals.fs +++ b/src/Compiler/TypedTree/TcGlobals.fs @@ -387,6 +387,13 @@ type TcGlobals( let sysCollections = ["System";"Collections"] let sysGenerics = ["System";"Collections";"Generic"] let sysCompilerServices = ["System";"Runtime";"CompilerServices"] + let sysThreadingTasks = ["System";"Threading";"Tasks"] + + // Task and ValueTask type refs for runtime-async support + let v_task_tcr = findSysTyconRef sysThreadingTasks "Task" + let v_genericTask_tcr = findSysTyconRef sysThreadingTasks "Task`1" + let v_valueTask_tcr = findSysTyconRef sysThreadingTasks "ValueTask" + let v_genericValueTask_tcr = findSysTyconRef sysThreadingTasks "ValueTask`1" let lazy_tcr = findSysTyconRef sys "Lazy`1" let v_fslib_IEvent2_tcr = mk_MFControl_tcref fslibCcu "IEvent`2" @@ -677,6 +684,9 @@ type TcGlobals( let mk_MFCompilerServices_attrib nm : BuiltinAttribInfo = AttribInfo(mkILTyRef(ilg.fsharpCoreAssemblyScopeRef, Core + "." + nm), mk_MFCompilerServices_tcref fslibCcu nm) + let mk_MFControl_attrib nm : BuiltinAttribInfo = + AttribInfo(mkILTyRef(ilg.fsharpCoreAssemblyScopeRef, ControlName + "." + nm), mk_MFControl_tcref fslibCcu nm) + let mkSourceDoc fileName = ILSourceDocument.Create(language=None, vendor=None, documentType=None, file=fileName) let compute i = @@ -1415,6 +1425,12 @@ type TcGlobals( member val mk_Attribute_ty = mkSysNonGenericTy sys "Attribute" member val system_LinqExpression_tcref = v_linqExpression_tcr + // Task and ValueTask type refs for runtime-async support + member val system_Task_tcref = v_task_tcr + member val system_GenericTask_tcref = v_genericTask_tcr + member val system_ValueTask_tcref = v_valueTask_tcr + member val system_GenericValueTask_tcref = v_genericValueTask_tcr + member val mk_IStructuralComparable_ty = mkSysNonGenericTy sysCollections "IStructuralComparable" member val mk_IStructuralEquatable_ty = mkSysNonGenericTy sysCollections "IStructuralEquatable" @@ -1554,6 +1570,7 @@ type TcGlobals( member val attrib_MeasureAttribute = mk_MFCore_attrib "MeasureAttribute" member val attrib_MeasureableAttribute = mk_MFCore_attrib "MeasureAnnotatedAbbreviationAttribute" member val attrib_NoDynamicInvocationAttribute = mk_MFCore_attrib "NoDynamicInvocationAttribute" + member val attrib_RuntimeAsyncAttribute = mk_MFControl_attrib "RuntimeAsyncAttribute" member val attrib_NoCompilerInliningAttribute = mk_MFCore_attrib "NoCompilerInliningAttribute" member val attrib_WarnOnWithoutNullArgumentAttribute = mk_MFCore_attrib "WarnOnWithoutNullArgumentAttribute" member val attrib_SecurityAttribute = tryFindSysAttrib "System.Security.Permissions.SecurityAttribute" diff --git a/src/Compiler/TypedTree/TcGlobals.fsi b/src/Compiler/TypedTree/TcGlobals.fsi index e69bc7b5e80..e660a116728 100644 --- a/src/Compiler/TypedTree/TcGlobals.fsi +++ b/src/Compiler/TypedTree/TcGlobals.fsi @@ -460,6 +460,9 @@ type internal TcGlobals = member attrib_RequiresLocationAttribute: BuiltinAttribInfo + member attrib_RuntimeAsyncAttribute: BuiltinAttribInfo + + member attrib_SealedAttribute: BuiltinAttribInfo member attrib_SecurityAttribute: BuiltinAttribInfo option @@ -1203,6 +1206,14 @@ type internal TcGlobals = member system_LinqExpression_tcref: TypedTree.EntityRef + member system_Task_tcref: TypedTree.EntityRef + + member system_GenericTask_tcref: TypedTree.EntityRef + + member system_ValueTask_tcref: TypedTree.EntityRef + + member system_GenericValueTask_tcref: TypedTree.EntityRef + member system_MarshalByRefObject_tcref: TypedTree.EntityRef option member system_MarshalByRefObject_ty: TypedTree.TType option diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index 154cdfbb025..d621310d57c 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -627,6 +627,11 @@ Sdílení podkladových polí v rozlišeném sjednocení [<Struct>] za předpokladu, že mají stejný název a typ + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules @@ -1762,6 +1767,26 @@ Použití obnovitelného kódu nebo obnovitelných stavových strojů vyžaduje /langversion:preview. + + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + + + + Methods marked with MethodImplOptions.Async cannot return byref types. + Methods marked with MethodImplOptions.Async cannot return byref types. + + + + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + + + + Methods marked with MethodImplOptions.Async are not supported in this context. + Methods marked with MethodImplOptions.Async are not supported in this context. + + Cannot call '{0}' - a setter for init-only property, please use object initialization instead. See https://aka.ms/fsharp-assigning-values-to-properties-at-initialization Nelze volat „{0}“ - metodu setter pro vlastnost pouze init. Použijte místo toho inicializaci objektu. Viz https://aka.ms/fsharp-assigning-values-to-properties-at-initialization diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index 2d7c8dcf008..4dcd5aae782 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -627,6 +627,11 @@ Teilen sie zugrunde liegende Felder in einen [<Struct>]-diskriminierten Union, solange sie denselben Namen und Typ aufweisen. + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules @@ -1762,6 +1767,26 @@ Die Verwendung von Fortsetzbarem Code oder fortsetzbaren Zustandscomputern erfordert /langversion:preview + + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + + + + Methods marked with MethodImplOptions.Async cannot return byref types. + Methods marked with MethodImplOptions.Async cannot return byref types. + + + + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + + + + Methods marked with MethodImplOptions.Async are not supported in this context. + Methods marked with MethodImplOptions.Async are not supported in this context. + + Cannot call '{0}' - a setter for init-only property, please use object initialization instead. See https://aka.ms/fsharp-assigning-values-to-properties-at-initialization „{0}“ kann nicht aufgerufen werden – ein Setter für die Eigenschaft nur für die Initialisierung. Bitte verwenden Sie stattdessen die Objektinitialisierung. Siehe https://aka.ms/fsharp-assigning-values-to-properties-at-initialization diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index 19e1fe88759..3df8eb7bcfd 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -627,6 +627,11 @@ Compartir campos subyacentes en una unión discriminada [<Struct>] siempre y cuando tengan el mismo nombre y tipo + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules @@ -1762,6 +1767,26 @@ El uso de código reanudable o de máquinas de estado reanudables requiere /langversion:preview + + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + + + + Methods marked with MethodImplOptions.Async cannot return byref types. + Methods marked with MethodImplOptions.Async cannot return byref types. + + + + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + + + + Methods marked with MethodImplOptions.Async are not supported in this context. + Methods marked with MethodImplOptions.Async are not supported in this context. + + Cannot call '{0}' - a setter for init-only property, please use object initialization instead. See https://aka.ms/fsharp-assigning-values-to-properties-at-initialization No se puede llamar a '{0}': un establecedor para una propiedad de solo inicialización. Use la inicialización del objeto en su lugar. Ver https://aka.ms/fsharp-assigning-values-to-properties-at-initialization diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index 98e2001eecf..c5d335ebd06 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -627,6 +627,11 @@ Partager les champs sous-jacents dans une union discriminée [<Struct>] tant qu’ils ont le même nom et le même type + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules @@ -1762,6 +1767,26 @@ L’utilisation de code pouvant être repris ou de machines d’état pouvant être reprises nécessite /langversion:preview + + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + + + + Methods marked with MethodImplOptions.Async cannot return byref types. + Methods marked with MethodImplOptions.Async cannot return byref types. + + + + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + + + + Methods marked with MethodImplOptions.Async are not supported in this context. + Methods marked with MethodImplOptions.Async are not supported in this context. + + Cannot call '{0}' - a setter for init-only property, please use object initialization instead. See https://aka.ms/fsharp-assigning-values-to-properties-at-initialization Nous n’avons pas pu appeler '{0}' - méthode setter pour la propriété init-only. Utilisez plutôt l’initialisation d’objet. Consultez https://aka.ms/fsharp-assigning-values-to-properties-at-initialization. diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index 5dade539805..34c0d34a1c5 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -627,6 +627,11 @@ Condividi i campi sottostanti in un'unione discriminata di [<Struct>] purché abbiano lo stesso nome e tipo + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules @@ -1762,6 +1767,26 @@ Per l'uso del codice ripristinabile o delle macchine a stati ripristinabili è richiesto /langversion:preview + + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + + + + Methods marked with MethodImplOptions.Async cannot return byref types. + Methods marked with MethodImplOptions.Async cannot return byref types. + + + + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + + + + Methods marked with MethodImplOptions.Async are not supported in this context. + Methods marked with MethodImplOptions.Async are not supported in this context. + + Cannot call '{0}' - a setter for init-only property, please use object initialization instead. See https://aka.ms/fsharp-assigning-values-to-properties-at-initialization Non è possibile chiamare '{0}', un setter per la proprietà init-only. Usare invece l'inizializzazione dell'oggetto. Vedere https://aka.ms/fsharp-assigning-values-to-properties-at-initialization diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index 500ab3771f0..9a0b2ed6347 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -627,6 +627,11 @@ 名前と型が同じである限り、[<Struct>] 判別可能な共用体で基になるフィールドを共有する + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules @@ -1762,6 +1767,26 @@ 再開可能なコードまたは再開可能なステート マシンを使用するには、/langversion:preview が必要です + + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + + + + Methods marked with MethodImplOptions.Async cannot return byref types. + Methods marked with MethodImplOptions.Async cannot return byref types. + + + + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + + + + Methods marked with MethodImplOptions.Async are not supported in this context. + Methods marked with MethodImplOptions.Async are not supported in this context. + + Cannot call '{0}' - a setter for init-only property, please use object initialization instead. See https://aka.ms/fsharp-assigning-values-to-properties-at-initialization '{0}' を呼び出すことはできません。これは init のみのプロパティのセッターなので、代わりにオブジェクトの初期化を使用してください。https://aka.ms/fsharp-assigning-values-to-properties-at-initialization を参照してください。 diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index a682e62da48..20e39bb9511 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -627,6 +627,11 @@ 이름과 형식이 같으면 [<Struct>] 구분된 공용 구조체에서 기본 필드 공유 + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules @@ -1762,6 +1767,26 @@ 다시 시작 가능한 코드 또는 다시 시작 가능한 상태 시스템을 사용하려면 /langversion:preview가 필요합니다. + + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + + + + Methods marked with MethodImplOptions.Async cannot return byref types. + Methods marked with MethodImplOptions.Async cannot return byref types. + + + + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + + + + Methods marked with MethodImplOptions.Async are not supported in this context. + Methods marked with MethodImplOptions.Async are not supported in this context. + + Cannot call '{0}' - a setter for init-only property, please use object initialization instead. See https://aka.ms/fsharp-assigning-values-to-properties-at-initialization init 전용 속성의 setter인 '{0}'을(를) 호출할 수 없습니다. 개체 초기화를 대신 사용하세요. https://aka.ms/fsharp-assigning-values-to-properties-at-initialization를 참조하세요. diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index 05159019056..1d2a18988e9 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -627,6 +627,11 @@ Udostępnij pola źródłowe w unii rozłącznej [<Struct>], o ile mają taką samą nazwę i ten sam typ + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules @@ -1762,6 +1767,26 @@ Używanie kodu z możliwością wznowienia lub automatów stanów z możliwością wznowienia wymaga parametru /langversion: wersja zapoznawcza + + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + + + + Methods marked with MethodImplOptions.Async cannot return byref types. + Methods marked with MethodImplOptions.Async cannot return byref types. + + + + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + + + + Methods marked with MethodImplOptions.Async are not supported in this context. + Methods marked with MethodImplOptions.Async are not supported in this context. + + Cannot call '{0}' - a setter for init-only property, please use object initialization instead. See https://aka.ms/fsharp-assigning-values-to-properties-at-initialization Nie można wywołać „{0}” — metody ustawiającej dla właściwości tylko do inicjowania. Zamiast tego użyj inicjowania obiektu. Zobacz https://aka.ms/fsharp-assigning-values-to-properties-at-initialization diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index 62d9febc99a..3054533e95d 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -627,6 +627,11 @@ Compartilhar campos subjacentes em uma união discriminada [<Struct>], desde que tenham o mesmo nome e tipo + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules @@ -1762,6 +1767,26 @@ Usar código retomável ou máquinas de estado retomável requer /langversion:preview + + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + + + + Methods marked with MethodImplOptions.Async cannot return byref types. + Methods marked with MethodImplOptions.Async cannot return byref types. + + + + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + + + + Methods marked with MethodImplOptions.Async are not supported in this context. + Methods marked with MethodImplOptions.Async are not supported in this context. + + Cannot call '{0}' - a setter for init-only property, please use object initialization instead. See https://aka.ms/fsharp-assigning-values-to-properties-at-initialization Não é possível chamar '{0}' – um setter da propriedade somente inicialização, use a inicialização de objeto. Confira https://aka.ms/fsharp-assigning-values-to-properties-at-initialization diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index 9c9d89c904f..89e101aa81d 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -627,6 +627,11 @@ Совместное использование базовых полей в дискриминируемом объединении [<Struct>], если они имеют одинаковое имя и тип. + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules @@ -1762,6 +1767,26 @@ Для использования возобновляемого кода или возобновляемых конечных автоматов требуется /langversion:preview + + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + + + + Methods marked with MethodImplOptions.Async cannot return byref types. + Methods marked with MethodImplOptions.Async cannot return byref types. + + + + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + + + + Methods marked with MethodImplOptions.Async are not supported in this context. + Methods marked with MethodImplOptions.Async are not supported in this context. + + Cannot call '{0}' - a setter for init-only property, please use object initialization instead. See https://aka.ms/fsharp-assigning-values-to-properties-at-initialization Не удается вызвать '{0}' — установщик для свойства только для инициализации, вместо этого используйте инициализацию объекта. См. https://aka.ms/fsharp-assigning-values-to-properties-at-initialization. diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index d0576bf131a..63138d79e74 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -627,6 +627,11 @@ Aynı ada ve türe sahip oldukları sürece temel alınan alanları [<Struct>] ayırt edici birleşim biçiminde paylaşın + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules @@ -1762,6 +1767,26 @@ Sürdürülebilir kod veya sürdürülebilir durum makinelerini kullanmak için /langversion:preview gerekir + + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + + + + Methods marked with MethodImplOptions.Async cannot return byref types. + Methods marked with MethodImplOptions.Async cannot return byref types. + + + + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + + + + Methods marked with MethodImplOptions.Async are not supported in this context. + Methods marked with MethodImplOptions.Async are not supported in this context. + + Cannot call '{0}' - a setter for init-only property, please use object initialization instead. See https://aka.ms/fsharp-assigning-values-to-properties-at-initialization Yalnızca başlatma özelliği için ayarlayıcı olan '{0}' çağrılamaz, lütfen bunun yerine nesne başlatmayı kullanın. bkz. https://aka.ms/fsharp-assigning-values-to-properties-at-initialization diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index f1e43c884d0..73bfebfd85b 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -627,6 +627,11 @@ 只要它们具有相同的名称和类型,即可在 [<Struct>] 中共享基础字段 + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules @@ -1762,6 +1767,26 @@ 使用可恢复代码或可恢复状态机需要 /langversion:preview + + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + + + + Methods marked with MethodImplOptions.Async cannot return byref types. + Methods marked with MethodImplOptions.Async cannot return byref types. + + + + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + + + + Methods marked with MethodImplOptions.Async are not supported in this context. + Methods marked with MethodImplOptions.Async are not supported in this context. + + Cannot call '{0}' - a setter for init-only property, please use object initialization instead. See https://aka.ms/fsharp-assigning-values-to-properties-at-initialization 无法调用 "{0}",它是仅限 init 属性的资源库,请改用对象初始化。请参阅 https://aka.ms/fsharp-assigning-values-to-properties-at-initialization diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index 48039c1413c..bc3790d5a90 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -627,6 +627,11 @@ 只要 [<Struct>] 具有相同名稱和類型,就以強制聯集共用基礎欄位 + + runtime async + runtime async + + Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules Support for scoped enabling / disabling of warnings by #warn and #nowarn directives, also inside modules @@ -1762,6 +1767,26 @@ 使用可繼續的程式碼或可繼續的狀態機器需要 /langversion:preview + + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + Methods marked with MethodImplOptions.Async cannot also use MethodImplOptions.Synchronized. + + + + Methods marked with MethodImplOptions.Async cannot return byref types. + Methods marked with MethodImplOptions.Async cannot return byref types. + + + + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + Methods marked with MethodImplOptions.Async must return Task, Task<'T>, ValueTask, or ValueTask<'T>. The actual return type is '{0}'. + + + + Methods marked with MethodImplOptions.Async are not supported in this context. + Methods marked with MethodImplOptions.Async are not supported in this context. + + Cannot call '{0}' - a setter for init-only property, please use object initialization instead. See https://aka.ms/fsharp-assigning-values-to-properties-at-initialization 無法呼叫 '{0}' - 僅初始化屬性的 setter,請改為使用物件初始化。請參閱 https://aka.ms/fsharp-assigning-values-to-properties-at-initialization diff --git a/src/FSharp.Core/FSharp.Core.fsproj b/src/FSharp.Core/FSharp.Core.fsproj index cad8ee1c930..a7c0ffe3664 100644 --- a/src/FSharp.Core/FSharp.Core.fsproj +++ b/src/FSharp.Core/FSharp.Core.fsproj @@ -223,6 +223,12 @@ Control/async.fs + + Control/runtimeAsync.fs + + + Control/runtimeTasks.fs + Control/tasks.fsi @@ -291,4 +297,27 @@ + + + + + netstandard + <_NETStandardLibRefPath>$(NuGetPackageRoot)netstandard.library\2.0.3\build\netstandard2.0\ref + + + + + + + + diff --git a/src/FSharp.Core/runtimeAsync.fs b/src/FSharp.Core/runtimeAsync.fs new file mode 100644 index 00000000000..db467e18a29 --- /dev/null +++ b/src/FSharp.Core/runtimeAsync.fs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +// Runtime-async attribute for library extensibility. +// RuntimeAsyncAttribute is available on all TFMs but is only meaningful on .NET 10.0+. +namespace Microsoft.FSharp.Control + +open System + +/// Marker attribute that the F# compiler reads to enable runtime-async semantics on a type or module. +/// The compiler uses this attribute for three purposes: +/// (1) Gating cil managed async IL flag emission — only methods whose enclosing type or module +/// carries [<RuntimeAsync>] receive the async IL flag (0x2000), whether via explicit +/// [<MethodImpl(0x2000)>] or via body analysis. +/// (2) Implicit NoDynamicInvocation — public members of a [<RuntimeAsync>]-marked class +/// automatically have their bodies replaced with a raise (NotSupportedException ...) in +/// compiled (non-inline) form, preventing unsafe dynamic invocation. +/// (3) Optimizer anti-inlining — the F# optimizer does not cross-module inline functions from +/// [<RuntimeAsync>]-marked types, preserving the cil managed async contract. +[] +type RuntimeAsyncAttribute() = + inherit Attribute() diff --git a/src/FSharp.Core/runtimeTasks.fs b/src/FSharp.Core/runtimeTasks.fs new file mode 100644 index 00000000000..8342bb31293 --- /dev/null +++ b/src/FSharp.Core/runtimeTasks.fs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +// RuntimeTaskBuilder - a computation expression builder for runtime-async methods. +// Only available on .NET 10.0+ where System.Runtime.CompilerServices.AsyncHelpers is available. +namespace Microsoft.FSharp.Control + +#if NET10_0_OR_GREATER + +open System +open System.Runtime.CompilerServices +open System.Threading.Tasks + +module internal RuntimeTaskBuilderUnsafe = + let inline cast<'a, 'b> (a: 'a) : 'b = (# "" a : 'b #) + +/// Computation expression builder for runtime-async methods. +/// Methods using this builder will have the async IL flag (0x2000) emitted. +/// All members are inline to produce flat method bodies (no state machine). +[] +type RuntimeTaskBuilder() = + member inline _.Return(x: 'T) : 'T = x + member inline _.Bind(t: Task<'T>, [] f: 'T -> 'U) : 'U = + f(AsyncHelpers.Await t) + member inline _.Bind(t: Task, [] f: unit -> 'U) : 'U = + AsyncHelpers.Await t + f() + member inline _.Bind(t: ValueTask<'T>, [] f: 'T -> 'U) : 'U = + f(AsyncHelpers.Await t) + member inline _.Bind(t: ValueTask, [] f: unit -> 'U) : 'U = + AsyncHelpers.Await t + f() + member inline _.Delay([] f: unit -> 'T) : 'T = f() + member inline _.Zero() : unit = () + member inline _.Combine((), [] f: unit -> 'T) : 'T = f() + member inline _.While([] guard: unit -> bool, [] body: unit -> unit) : unit = + while guard() do body() + member inline _.For(s: seq<'T>, [] body: 'T -> unit) : unit = + for x in s do body(x) + member inline _.TryWith([] body: unit -> 'T, [] handler: exn -> 'T) : 'T = + try body() with e -> handler e + member inline _.TryFinally([] body: unit -> 'T, [] comp: unit -> unit) : 'T = + try body() finally comp() + member inline _.Using(resource: 'T when 'T :> IDisposable, [] body: 'T -> 'U) : 'U = + try body resource finally (resource :> IDisposable).Dispose() + [ 0x2000)>] + member inline _.Run([] f: unit -> 'T) : Task<'T> = + RuntimeTaskBuilderUnsafe.cast (f()) + +[] +module RuntimeTaskBuilderModule = + /// Computation expression for runtime-async methods. + /// Produces flat IL bodies using AsyncHelpers.Await (no state machine). + let runtimeTask = RuntimeTaskBuilder() +#endif diff --git a/tests/FSharp.Compiler.ComponentTests/EmittedIL/MethodImplAttribute/MethodImplAttribute.fs b/tests/FSharp.Compiler.ComponentTests/EmittedIL/MethodImplAttribute/MethodImplAttribute.fs index 7f949913cab..f0b46d33d80 100644 --- a/tests/FSharp.Compiler.ComponentTests/EmittedIL/MethodImplAttribute/MethodImplAttribute.fs +++ b/tests/FSharp.Compiler.ComponentTests/EmittedIL/MethodImplAttribute/MethodImplAttribute.fs @@ -3,6 +3,7 @@ namespace EmittedIL open Xunit open FSharp.Test open FSharp.Test.Compiler +open System module MethodImplAttribute = @@ -80,3 +81,554 @@ module MethodImplAttribute = compilation |> getCompilation |> verifyCompilation + + // ===================================================================================== + // Task 14: IL baseline tests for RuntimeAsync feature + // Verify that [] emits 'cil managed async' in IL + // ===================================================================================== + + // Verify that a simple async method with MethodImplOptions.Async emits the async IL flag. + // The body returns int directly (runtime-async: body is type-checked against T, not Task). + [] + let ``RuntimeAsync - method with Async attribute emits cil managed async in IL``() = + FSharp """ +[] +module TestModule + +#nowarn "57" +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +let asyncMethod () : Task = 42 +""" + |> withLangVersionPreview + |> compile + |> shouldSucceed + |> verifyIL [ + // The method must carry the 'async' flag in its IL method header + """cil managed async""" + ] + + // Verify that the async flag is present on a method returning Task (non-generic). + [] + let ``RuntimeAsync - Task-returning method emits cil managed async in IL``() = + FSharp """ +[] +module TestModule + +#nowarn "57" +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +let asyncVoidMethod () : Task = () +""" + |> withLangVersionPreview + |> compile + |> shouldSucceed + |> verifyIL [ + """cil managed async""" + ] + + // ===================================================================================== + // Task 15: Validation error tests for RuntimeAsync feature + // ===================================================================================== + + // Error 3885: async method cannot also be synchronized (0x2020 = 0x2000 | 0x20) + [] + let ``RuntimeAsync - error when async method is also synchronized``() = + FSharp """ +module TestModule + +open System.Runtime.CompilerServices +open System.Threading.Tasks + +// Note: 0x2020 = Async (0x2000) + Synchronized (0x20) +[(0x2020))>] +let invalidMethod () : Task = Task.FromResult(42) +""" + |> withLangVersionPreview + |> typecheck + |> shouldFail + |> withDiagnosticMessageMatches "cannot also use" + + // Error 3884: async method must return Task, Task, ValueTask, or ValueTask + [] + let ``RuntimeAsync - error when async method does not return a Task type``() = + FSharp """ +module TestModule + +open System.Runtime.CompilerServices + +[(0x2000))>] +let invalidMethod () : int = 42 +""" + |> withLangVersionPreview + |> typecheck + |> shouldFail + |> withDiagnosticMessageMatches "must return Task" + + // Feature gate: using MethodImplOptions.Async without --langversion:preview emits a preview feature error + [] + let ``RuntimeAsync - error when Async attribute used without preview langversion``() = + FSharp """ +module TestModule + +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[(0x2000))>] +let asyncMethod () : Task = Task.FromResult(42) +""" + |> withLangVersion90 + |> typecheck + |> shouldFail + |> withDiagnosticMessageMatches "runtime async" + + // ===================================================================================== + // Task 16: Behavioral tests for RuntimeAsync feature + // Verify that methods with [] actually execute correctly at runtime + // ===================================================================================== + + // Behavioral test: simple return — method body returns T directly, Task is produced by runtime + [] + let ``RuntimeAsync - behavioral test: simple return``() = + // DOTNET_RuntimeAsync=1 must be set in the test process before the compiled assembly is loaded. + // Setting it inside the compiled code is too late (the CLR loads the type before any code runs). + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + FSharp """ +[] +module TestModule + +#nowarn "57" +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +let asyncReturn42 () : Task = 42 + +let result = asyncReturn42().Result +printfn "%d" result +""" + |> withLangVersionPreview + |> compileExeAndRunNewProcess + |> shouldSucceed + |> withOutputContainsAllInOrder ["42"] + + // Behavioral test: await Task — method awaits a Task and returns the result + [] + let ``RuntimeAsync - behavioral test: await Task``() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + FSharp """ +[] +module TestModule + +#nowarn "57" +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +let asyncAwaitTask () : Task = AsyncHelpers.Await(Task.FromResult(42)) + +let result = asyncAwaitTask().Result +printfn "%d" result +""" + |> withLangVersionPreview + |> compileExeAndRunNewProcess + |> shouldSucceed + |> withOutputContainsAllInOrder ["42"] + + // Behavioral test: await Task (unit) — method awaits a non-generic Task + [] + let ``RuntimeAsync - behavioral test: await Task (unit)``() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + FSharp """ +[] +module TestModule + +#nowarn "57" +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +let asyncAwaitUnitTask () : Task = AsyncHelpers.Await(Task.CompletedTask) + +asyncAwaitUnitTask().Wait() +printfn "done" +""" + |> withLangVersionPreview + |> compileExeAndRunNewProcess + |> shouldSucceed + |> withOutputContainsAllInOrder ["done"] + + // Behavioral test: await ValueTask — method awaits a ValueTask + [] + let ``RuntimeAsync - behavioral test: await ValueTask``() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + FSharp """ +[] +module TestModule + +#nowarn "57" +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +let asyncAwaitValueTask () : Task = AsyncHelpers.Await(ValueTask.FromResult(42)) + +let result = asyncAwaitValueTask().Result +printfn "%d" result +""" + |> withLangVersionPreview + |> compileExeAndRunNewProcess + |> shouldSucceed + |> withOutputContainsAllInOrder ["42"] + + // Behavioral test: multiple awaits — method awaits two tasks and adds results + [] + let ``RuntimeAsync - behavioral test: multiple awaits``() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + FSharp """ +[] +module TestModule + +#nowarn "57" +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +let asyncMultipleAwaits () : Task = + let a = AsyncHelpers.Await(Task.FromResult(10)) + let b = AsyncHelpers.Await(Task.FromResult(32)) + a + b + +let result = asyncMultipleAwaits().Result +printfn "%d" result +""" + |> withLangVersionPreview + |> compileExeAndRunNewProcess + |> shouldSucceed + |> withOutputContainsAllInOrder ["42"] + + // ===================================================================================== + // Task 18: Edge case tests for RuntimeAsync feature + // ===================================================================================== + + // Edge case: generic method — method is generic over the awaited type + [] + let ``RuntimeAsync - edge case: generic method``() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + FSharp """ +[] +module TestModule + +#nowarn "57" +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +let genericAsync<'T> (t: Task<'T>) : Task<'T> = AsyncHelpers.Await t + +let result = genericAsync(Task.FromResult(42)).Result +printfn "%d" result +""" + |> withLangVersionPreview + |> compileExeAndRunNewProcess + |> shouldSucceed + |> withOutputContainsAllInOrder ["42"] + + // Edge case: try/with — method uses try/with and succeeds (no exception) + [] + let ``RuntimeAsync - edge case: try/with success``() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + FSharp """ +[] +module TestModule + +#nowarn "57" +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +let asyncTryWith () : Task = + try AsyncHelpers.Await(Task.FromResult(42)) + with _ -> -1 + +let result = asyncTryWith().Result +printfn "%d" result +""" + |> withLangVersionPreview + |> compileExeAndRunNewProcess + |> shouldSucceed + |> withOutputContainsAllInOrder ["42"] + + // Edge case: try/with exception — method catches an exception and returns fallback + [] + let ``RuntimeAsync - edge case: try/with exception``() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + FSharp """ +[] +module TestModule + +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +let asyncTryWithException () : Task = + try failwith "oops" + with _ -> -1 + +let result = asyncTryWithException().Result +printfn "%d" result +""" + |> withLangVersionPreview + |> compileExeAndRunNewProcess + |> shouldSucceed + |> withOutputContainsAllInOrder ["-1"] + + // Edge case: interop with task CE — method awaits a task CE result + [] + let ``RuntimeAsync - edge case: interop with task CE``() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + FSharp """ +[] +module TestModule + +#nowarn "57" +open System.Runtime.CompilerServices +open System.Threading.Tasks + +[] +let asyncInteropWithTaskCE () : Task = + let t = task { return 42 } + AsyncHelpers.Await t + +let result = asyncInteropWithTaskCE().Result +printfn "%d" result +""" + |> withLangVersionPreview + |> compileExeAndRunNewProcess + |> shouldSucceed + |> withOutputContainsAllInOrder ["42"] + + // RuntimeAsync attribute on builder class implicitly applies NoDynamicInvocation to all + // public inline members. Their IL bodies are replaced with 'throw NotSupportedException'. + // Uses the new cast-free builder: Delay is 'fun () -> f()' and Run is non-inline with + // [] — no cast helper or sentinel needed in user code. + [] + let ``RuntimeAsync - implicit NoDynamicInvocation on builder inline members``() = + FSharp """ +module TestModule + +#nowarn "57" +open System +open System.Runtime.CompilerServices +open System.Threading.Tasks +open Microsoft.FSharp.Control + +[] +type RuntimeTaskBuilder() = + member inline _.Return(x: 'T) : 'T = x + member inline _.Bind(t: Task<'T>, [] f: 'T -> 'U) : 'U = + f(AsyncHelpers.Await t) + member inline _.Delay([] f: unit -> 'T) : unit -> Task<'T> = fun () -> f() + member inline _.Zero() : unit = () + [ 0x2000)>] + member _.Run(f: unit -> Task<'T>) : Task<'T> = AsyncHelpers.Await(f()) + +[] +module RuntimeTaskBuilderModule = + let runtimeTask = RuntimeTaskBuilder() +""" + |> withLangVersionPreview + |> compile + |> shouldSucceed + |> verifyIL [ + // Bind's IL body must be replaced with throw NotSupportedException + // (implicit NoDynamicInvocation from [] on the declaring type) + "\"Dynamic invocation of Bind is not supported\"" + ] + + // With the cast-free builder, Run is non-inline [] and is + // 'cil managed async'. The Delay closure also becomes 'cil managed async' via the + // auto-injected sentinel. 'cil managed async' appears in the IL output for both. + [] + let ``RuntimeAsync - consumer function gets cil managed async without MethodImpl attribute``() = + FSharp """ +module TestModule + +#nowarn "57" +open System +open System.Runtime.CompilerServices +open System.Threading.Tasks +open Microsoft.FSharp.Control + +[] +type RuntimeTaskBuilder() = + member inline _.Return(x: 'T) : 'T = x + member inline _.Bind(t: Task<'T>, [] f: 'T -> 'U) : 'U = + f(AsyncHelpers.Await t) + member inline _.Delay([] f: unit -> 'T) : unit -> Task<'T> = fun () -> f() + member inline _.Zero() : unit = () + [ 0x2000)>] + member _.Run(f: unit -> Task<'T>) : Task<'T> = AsyncHelpers.Await(f()) + +[] +module RuntimeTaskBuilderModule = + let runtimeTask = RuntimeTaskBuilder() + +// No [] on the consumer — Run carries 'cil managed async'. +// The Delay closure is also 'cil managed async' due to auto-injected sentinel. +let myConsumer () : Task = + runtimeTask { + let! x = Task.FromResult(42) + return x + } +""" + |> withLangVersionPreview + |> compile + |> shouldSucceed + |> verifyIL [ + // Run is cil managed async (MethodImplOptions.Async), and the Delay closure + // Invoke is also cil managed async (auto-injected sentinel ensures cloIsAsync=true). + """cil managed async""" + ] + + // Behavioral test: consumer function using cast-free [] builder executes correctly. + // Run is non-inline []. Delay is 'fun () -> f()' (no cast helper). + // The compiler auto-injects the sentinel and handles 'T → Task<'T> bridging automatically. + [] + let ``RuntimeAsync - behavioral test: consumer with RuntimeAsync builder``() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + FSharp """ +module TestModule + +#nowarn "57" +open System +open System.Runtime.CompilerServices +open System.Threading.Tasks +open Microsoft.FSharp.Control + +[] +type RuntimeTaskBuilder() = + member inline _.Return(x: 'T) : 'T = x + member inline _.Bind(t: Task<'T>, [] f: 'T -> 'U) : 'U = + f(AsyncHelpers.Await t) + member inline _.Delay([] f: unit -> 'T) : unit -> Task<'T> = fun () -> f() + member inline _.Zero() : unit = () + [ 0x2000)>] + member _.Run(f: unit -> Task<'T>) : Task<'T> = AsyncHelpers.Await(f()) + +[] +module RuntimeTaskBuilderModule = + let runtimeTask = RuntimeTaskBuilder() + +let myConsumer () : Task = + runtimeTask { + let! x = Task.FromResult(21) + let! y = Task.FromResult(21) + return x + y + } + +let result = myConsumer().Result +printfn "%d" result +""" + |> withLangVersionPreview + |> compileExeAndRunNewProcess + |> shouldSucceed + |> withOutputContainsAllInOrder ["42"] + + // ===================================================================================== + // Task 8: Cast-free builder architecture tests + // Verify that the compiler's automatic bridging works: Delay uses 'fun () -> f()' with + // no cast helper, and the Delay closure is emitted as 'cil managed async' by the compiler. + // ===================================================================================== + + // Verify that a [] builder with cast-free Delay ('fun () -> f()') compiles + // successfully and the Delay closure's Invoke is emitted as 'cil managed async'. + // The compiler auto-injects the sentinel into the Delay closure body and handles the + // 'T → Task<'T> return-type bridging automatically — no cast helper required. + [] + let ``RuntimeAsync - cast-free Delay closure is emitted as cil managed async``() = + FSharp """ +module TestModule + +#nowarn "57" +open System.Runtime.CompilerServices +open System.Threading.Tasks +open Microsoft.FSharp.Control + +[] +type RuntimeTaskBuilder() = + member inline _.Return(x: 'T) : 'T = x + member inline _.Bind(t: Task<'T>, [] f: 'T -> 'U) : 'U = + f(AsyncHelpers.Await t) + // Cast-free Delay: compiler handles 'T -> Task<'T> bridging and injects sentinel automatically. + member inline _.Delay([] f: unit -> 'T) : unit -> Task<'T> = fun () -> f() + member inline _.Zero() : unit = () + [ 0x2000)>] + member _.Run(f: unit -> Task<'T>) : Task<'T> = AsyncHelpers.Await(f()) + +[] +module RuntimeTaskBuilderModule = + let runtimeTask = RuntimeTaskBuilder() + +let useBuilder () : Task = + runtimeTask { + let! x = Task.FromResult(42) + return x + } +""" + |> withLangVersionPreview + |> compile + |> shouldSucceed + |> verifyIL [ + // The Delay closure's Invoke must be 'cil managed async': + // the compiler auto-injects AsyncHelpers.Await(ValueTask.CompletedTask) sentinel, + // which sets cloIsAsync=true, causing EraseClosures.fs to emit async Invoke. + """cil managed async""" + ] + + // Verify that a CE with no async operations ('runtimeTask { return 42 }') still produces + // a Delay closure emitted as 'cil managed async'. The compiler auto-injects the sentinel + // into ALL Delay closures for [] builders, even when the body has no let!/do!. + [] + let ``RuntimeAsync - CE with no async ops produces cil managed async closure via sentinel``() = + FSharp """ +module TestModule + +#nowarn "57" +open System.Runtime.CompilerServices +open System.Threading.Tasks +open Microsoft.FSharp.Control + +[] +type RuntimeTaskBuilder() = + member inline _.Return(x: 'T) : 'T = x + member inline _.Bind(t: Task<'T>, [] f: 'T -> 'U) : 'U = + f(AsyncHelpers.Await t) + member inline _.Delay([] f: unit -> 'T) : unit -> Task<'T> = fun () -> f() + member inline _.Zero() : unit = () + [ 0x2000)>] + member _.Run(f: unit -> Task<'T>) : Task<'T> = AsyncHelpers.Await(f()) + +[] +module RuntimeTaskBuilderModule = + let runtimeTask = RuntimeTaskBuilder() + +// No let!/do! — pure return. Sentinel injection ensures the Delay closure is still async. +let pureReturn () : Task = + runtimeTask { + return 42 + } +""" + |> withLangVersionPreview + |> compile + |> shouldSucceed + |> verifyIL [ + // Even with no AsyncHelpers.Await in the user CE body, the compiler-injected + // sentinel (AsyncHelpers.Await(ValueTask.CompletedTask)) forces cloIsAsync=true, + // so the Delay closure Invoke is still emitted as 'cil managed async'. + """cil managed async""" + ] diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index dda4aa613b4..ee1becf5c30 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -754,6 +754,7 @@ FSharp.Compiler.AbstractIL.IL+ILMemberAccess: System.String ToString() FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean HasSecurity FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean IsAbstract FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean IsAggressiveInline +FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean IsAsync FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean IsCheckAccessOnOverride FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean IsClassInitializer FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean IsConstructor @@ -779,6 +780,7 @@ FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean IsZeroInit FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean get_HasSecurity() FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean get_IsAbstract() FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean get_IsAggressiveInline() +FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean get_IsAsync() FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean get_IsCheckAccessOnOverride() FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean get_IsClassInitializer() FSharp.Compiler.AbstractIL.IL+ILMethodDef: Boolean get_IsConstructor() diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl index 217d4b7c837..a141360c9ab 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl @@ -603,8 +603,8 @@ Microsoft.FSharp.Collections.SetModule: Microsoft.FSharp.Collections.FSharpSet`1 Microsoft.FSharp.Collections.SetModule: Microsoft.FSharp.Collections.FSharpSet`1[T] UnionMany[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Collections.FSharpSet`1[T]]) Microsoft.FSharp.Collections.SetModule: Microsoft.FSharp.Collections.FSharpSet`1[T] Union[T](Microsoft.FSharp.Collections.FSharpSet`1[T], Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: System.Collections.Generic.IEnumerable`1[T] ToSeq[T](Microsoft.FSharp.Collections.FSharpSet`1[T]) -Microsoft.FSharp.Collections.SetModule: System.Tuple`2[Microsoft.FSharp.Collections.FSharpSet`1[T],Microsoft.FSharp.Collections.FSharpSet`1[T]] Partition[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: System.Tuple`2[Microsoft.FSharp.Collections.FSharpSet`1[T1],Microsoft.FSharp.Collections.FSharpSet`1[T2]] PartitionWith[T,T1,T2](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpChoice`2[T1,T2]], Microsoft.FSharp.Collections.FSharpSet`1[T]) +Microsoft.FSharp.Collections.SetModule: System.Tuple`2[Microsoft.FSharp.Collections.FSharpSet`1[T],Microsoft.FSharp.Collections.FSharpSet`1[T]] Partition[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: T MaxElement[T](Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: T MinElement[T](Microsoft.FSharp.Collections.FSharpSet`1[T]) Microsoft.FSharp.Collections.SetModule: TState FoldBack[T,TState](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpFunc`2[TState,TState]], Microsoft.FSharp.Collections.FSharpSet`1[T], TState) @@ -745,6 +745,7 @@ Microsoft.FSharp.Control.ObservableModule: System.IObservable`1[T] Merge[T](Syst Microsoft.FSharp.Control.ObservableModule: System.Tuple`2[System.IObservable`1[TResult1],System.IObservable`1[TResult2]] Split[T,TResult1,TResult2](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpChoice`2[TResult1,TResult2]], System.IObservable`1[T]) Microsoft.FSharp.Control.ObservableModule: System.Tuple`2[System.IObservable`1[T],System.IObservable`1[T]] Partition[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], System.IObservable`1[T]) Microsoft.FSharp.Control.ObservableModule: Void Add[T](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.Unit], System.IObservable`1[T]) +Microsoft.FSharp.Control.RuntimeAsyncAttribute: Void .ctor() Microsoft.FSharp.Control.TaskBuilder: System.Threading.Tasks.Task`1[T] RunDynamic[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T]) Microsoft.FSharp.Control.TaskBuilder: System.Threading.Tasks.Task`1[T] Run[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T]) Microsoft.FSharp.Control.TaskBuilderBase: Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[TOverall],Microsoft.FSharp.Core.Unit] For[T,TOverall](System.Collections.Generic.IEnumerable`1[T], Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[TOverall],Microsoft.FSharp.Core.Unit]]) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl index ed913ea04d3..adab9985400 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl @@ -747,6 +747,7 @@ Microsoft.FSharp.Control.ObservableModule: System.IObservable`1[T] Merge[T](Syst Microsoft.FSharp.Control.ObservableModule: System.Tuple`2[System.IObservable`1[TResult1],System.IObservable`1[TResult2]] Split[T,TResult1,TResult2](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpChoice`2[TResult1,TResult2]], System.IObservable`1[T]) Microsoft.FSharp.Control.ObservableModule: System.Tuple`2[System.IObservable`1[T],System.IObservable`1[T]] Partition[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], System.IObservable`1[T]) Microsoft.FSharp.Control.ObservableModule: Void Add[T](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.Unit], System.IObservable`1[T]) +Microsoft.FSharp.Control.RuntimeAsyncAttribute: Void .ctor() Microsoft.FSharp.Control.TaskBuilder: System.Threading.Tasks.Task`1[T] RunDynamic[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T]) Microsoft.FSharp.Control.TaskBuilder: System.Threading.Tasks.Task`1[T] Run[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T]) Microsoft.FSharp.Control.TaskBuilderBase: Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[TOverall],Microsoft.FSharp.Core.Unit] For[T,TOverall](System.Collections.Generic.IEnumerable`1[T], Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[TOverall],Microsoft.FSharp.Core.Unit]]) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj b/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj index d4ff59d3cbd..6cd9c0803ab 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.UnitTests.fsproj @@ -80,6 +80,7 @@ + diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/RuntimeTasks.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/RuntimeTasks.fs new file mode 100644 index 00000000000..5e3237e5865 --- /dev/null +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/RuntimeTasks.fs @@ -0,0 +1,615 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +// Tests for RuntimeTaskBuilder (runtimeTask computation expression) +// Uses CompilerAssert.ExecuteAux because FSharp.Test.Compiler.FSharp cannot be used from +// FSharp.Core.UnitTests (Range.setTestSource is internal to FSharp.Compiler.Service). +// +// Design notes: +// - The preamble defines RuntimeTaskBuilder with [] on the type. +// Delay returns unit -> 'T (a thunk) and Run returns Task<'T> (not 'T). +// - Consumer functions need NO [] — the [] attribute +// on RuntimeTaskBuilder causes the compiler to automatically emit 'cil managed async' on +// consumer functions via the inlined Await sentinel in Run. +// - [] on the builder class implicitly applies NoDynamicInvocation to all public +// inline members, preventing the F# compiler from adding the cil managed async flag to the +// Bind IL methods. Without it, the CLR would reject RuntimeTaskBuilder because Bind has the +// async flag (0x2000) but returns 'U (not Task<'U>). +// - DOTNET_RuntimeAsync=1 is set in the test process; child processes inherit it via +// psi.EnvironmentVariables (populated from current process env). + +namespace FSharp.Core.UnitTests.Control.RuntimeTasks + +open System +open Xunit +open FSharp.Test + +#if NET10_0_OR_GREATER + +module private RuntimeTaskTestHelpers = + + /// Preamble that defines RuntimeTaskBuilder inline. + /// Uses [] on RuntimeTaskBuilder. Delay returns unit -> 'T (a thunk). + /// Run returns Task<'T> with an Await sentinel + cast trick. + /// Consumer functions need NO [] — [] on the + /// builder class causes the compiler to automatically emit 'cil managed async'. + let private preamble = + "open System\n" + + "open System.Runtime.CompilerServices\n" + + "open System.Threading.Tasks\n" + + "open Microsoft.FSharp.Control\n" + + "\n" + + "#nowarn \"57\"\n" + + "#nowarn \"42\"\n" + + "\n" + + "module internal RuntimeTaskBuilderHelpers =\n" + + " let inline cast<'a, 'b> (a: 'a) : 'b = (# \"\" a : 'b #)\n" + + "\n" + + "[]\n" + + "type RuntimeTaskBuilder() =\n" + + " member inline _.Return(x: 'T) : 'T = x\n" + + " member inline _.Bind(t: Task<'T>, [] f: 'T -> 'U) : 'U =\n" + + " f(AsyncHelpers.Await t)\n" + + " member inline _.Bind(t: Task, [] f: unit -> 'U) : 'U =\n" + + " AsyncHelpers.Await t\n" + + " f()\n" + + " member inline _.Bind(t: ValueTask<'T>, [] f: 'T -> 'U) : 'U =\n" + + " f(AsyncHelpers.Await t)\n" + + " member inline _.Bind(t: ValueTask, [] f: unit -> 'U) : 'U =\n" + + " AsyncHelpers.Await t\n" + + " f()\n" + + " member inline _.Delay(f: unit -> 'T) : unit -> 'T = f\n" + + " member inline _.Zero() : unit = ()\n" + + " member inline _.Combine((): unit, [] f: unit -> 'T) : 'T = f()\n" + + " member inline _.While([] guard: unit -> bool, [] body: unit -> unit) : unit =\n" + + " while guard() do body()\n" + + " member inline _.For(s: seq<'T>, [] body: 'T -> unit) : unit =\n" + + " for x in s do body(x)\n" + + " member inline _.TryWith([] body: unit -> 'T, [] handler: exn -> 'T) : 'T =\n" + + " try body() with e -> handler e\n" + + " member inline _.TryFinally([] body: unit -> 'T, [] comp: unit -> unit) : 'T =\n" + + " try body() finally comp()\n" + + " member inline _.Using(resource: 'T when 'T :> IDisposable, [] body: 'T -> 'U) : 'U =\n" + + " try body resource finally (resource :> IDisposable).Dispose()\n" + + " // Run is fully inline — its body (including the Await sentinel and cast) gets inlined\n" + + " // into each consumer function. No [] needed on consumers.\n" + + " member inline _.Run([] f: unit -> 'T) : Task<'T> =\n" + + " AsyncHelpers.Await(ValueTask.CompletedTask)\n" + + " RuntimeTaskBuilderHelpers.cast (f())\n" + + "\n" + + "[]\n" + + "module RuntimeTaskBuilderModule =\n" + + " let runtimeTask = RuntimeTaskBuilder()\n" + + "\n" + + /// Helper: compile and run an F# program that uses runtimeTask { }. + /// DOTNET_RuntimeAsync=1 must be set before calling this so child processes inherit it. + let runTest (expectedOutputs: string list) (body: string) = + let source = preamble + body + let cmpl = Compilation.Create(source, CompileOutput.Exe, options = [| "--langversion:preview"; "--nowarn:3541" |]) + // beforeExecute: copy deps AND FSharp.Core to the output dir so the child process can find them. + // CompilerAssert.ExecuteAux only copies the explicit deps list, which doesn't include FSharp.Core + // (it's referenced via -r: in defaultProjectOptions, not as a sub-compilation dep). + let beforeExecute (outputFilePath: string) (deps: string list) = + let outputDirectory = System.IO.Path.GetDirectoryName(outputFilePath) + let copyIfMissing (src: string) = + let destPath = System.IO.Path.Combine(outputDirectory, System.IO.Path.GetFileName(src)) + if not (System.IO.File.Exists(destPath)) then + System.IO.File.Copy(src, destPath) + for dep in deps do + copyIfMissing dep + // FSharp.Core is not in deps; copy it explicitly. + let fsharpCoreLocation = typeof.Assembly.Location + copyIfMissing fsharpCoreLocation + let outcome, stdout, _stderr = CompilerAssert.ExecuteAux(cmpl, ignoreWarnings = true, beforeExecute = beforeExecute, newProcess = true) + match outcome with + | Failure exn -> failwith $"Execution failed: {exn.Message}" + | ExitCode n when n <> 0 -> failwith $"Process exited with code {n}.\nStdout: {stdout}\nStderr: {_stderr}" + | _ -> + for expected in expectedOutputs do + if not (stdout.Contains(expected)) then + failwith $"Expected output to contain '{expected}', but got:\n{stdout}" +type SmokeTestsForCompilation() = + + [] + member _.tinyRuntimeTask() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["1"] """ +let run () : Task = runtimeTask { return 1 } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.tbind() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["2"] """ +let run () : Task = + runtimeTask { + let! x = Task.FromResult(1) + return 1 + x + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.tnested() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["1"] """ +let inner () : Task = runtimeTask { return 1 } +let run () : Task = + runtimeTask { + let! x = inner() + return x + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.tcatch0() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["1"] """ +let run () : Task = + runtimeTask { + try + return 1 + with _ -> + return 2 + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.tcatch1() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["1"] """ +let run () : Task = + runtimeTask { + try + let! x = Task.FromResult(1) + return x + with _ -> + return 2 + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.tbindTask() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["2"] """ +let run () : Task = + runtimeTask { + let! x = Task.FromResult(1) + return x + 1 + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.tbindUnitTask() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["1"] """ +let run () : Task = + runtimeTask { + do! Task.CompletedTask + return 1 + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.tbindValueTask() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["2"] """ +let run () : Task = + runtimeTask { + let! x = ValueTask.FromResult(1) + return x + 1 + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.tbindUnitValueTask() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["1"] """ +let run () : Task = + runtimeTask { + do! ValueTask.CompletedTask + return 1 + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.twhile() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["5"] """ +let mutable i = 0 +let run () : Task = + runtimeTask { + while i < 5 do + i <- i + 1 + return i + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.tfor() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["done"] """ +let mutable total = 0 +// Note: Task return type with For causes InvalidProgramException (compiler generates generic method). +// Use Task with an explicit return to avoid this. +let run () : Task = + runtimeTask { + for x in [1; 2; 3] do + total <- total + x + return total + } +run().Wait() +printfn "done" +""" + + [] + member _.tusing() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["disposed"] """ +let mutable disposed = false +// Note: Task return type with Using causes InvalidProgramException (compiler generates generic method). +// Use Task with an explicit return to avoid this. +let run () : Task = + runtimeTask { + use _ = { new System.IDisposable with member _.Dispose() = disposed <- true } + return 0 + } +run().Wait() +if disposed then printfn "disposed" +""" + +type Basics() = + + [] + member _.testShortCircuitResult() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["3"] """ +let run () : Task = + runtimeTask { + let! x = Task.FromResult(1) + let! y = Task.FromResult(2) + return x + y + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.testCatching1() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["x=0 y=1"] """ +let mutable x = 0 +let mutable y = 0 +let run () : Task = + runtimeTask { + try + failwith "hello" + x <- 1 + with _ -> + () + y <- 1 + return 0 + } +run().Wait() +printfn "x=%d y=%d" x y +""" + + [] + member _.testCatching2() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["x=0 y=1"] """ +let mutable x = 0 +let mutable y = 0 +let run () : Task = + runtimeTask { + try + let! _ = Task.FromResult(0) + failwith "hello" + x <- 1 + with _ -> + () + y <- 1 + return 0 + } +run().Wait() +printfn "x=%d y=%d" x y +""" + + [] + member _.testNestedCatching() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["inner=1 outer=2"] """ +let mutable counter = 1 +let mutable caughtInner = 0 +let mutable caughtOuter = 0 +let t1 () : Task = + runtimeTask { + try + failwith "hello" + return 0 + with e -> + caughtInner <- counter + counter <- counter + 1 + raise e + return 0 + } +let t2 () : Task = + runtimeTask { + try + let! _ = t1() + return 0 + with _ -> + caughtOuter <- counter + return 0 + } +try (t2()).Wait() with _ -> () +printfn "inner=%d outer=%d" caughtInner caughtOuter +""" + + [] + member _.testWhileLoop() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["10"] """ +let mutable i = 0 +let run () : Task = + runtimeTask { + while i < 10 do + i <- i + 1 + return i + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.testTryFinallyHappyPath() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["ran=true"] """ +let mutable ran = false +let run () : Task = + runtimeTask { + try + let! _ = Task.FromResult(1) + () + finally + ran <- true + return 0 + } +run().Wait() +printfn "ran=%b" ran +""" + + [] + member _.testTryFinallySadPath() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["ran=true"] """ +let mutable ran = false +let run () : Task = + runtimeTask { + try + failwith "uhoh" + return 0 + finally + ran <- true + } +try run().Wait() with _ -> () +printfn "ran=%b" ran +""" + + [] + member _.testTryFinallyCaught() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["result=2 ran=true"] """ +let mutable ran = false +let run () : Task = + runtimeTask { + try + try + let! _ = Task.FromResult(1) + failwith "uhoh" + finally + ran <- true + return 1 + with _ -> + return 2 + } +let t = run() +printfn "result=%d ran=%b" t.Result ran +""" + + [] + member _.testUsing() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["disposed=true"] """ +let mutable disposed = false +let run () : Task = + runtimeTask { + use _ = { new System.IDisposable with member _.Dispose() = disposed <- true } + let! _ = Task.FromResult(1) + return 1 + } +let t = run() +t.Wait() +printfn "disposed=%b" disposed +""" + + [] + member _.testForLoop() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["sum=6"] """ +let mutable sum = 0 +let run () : Task = + runtimeTask { + for i in [1; 2; 3] do + sum <- sum + i + return sum + } +let t = run() +t.Wait() +printfn "sum=%d" t.Result +""" + + [] + member _.testExceptionAttachedToTask() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["got exception: boom"] """ +let run () : Task = + runtimeTask { + failwith "boom" + return 1 + } +let t = run() +try + t.Wait() + printfn "no exception" +with +| :? System.AggregateException as ae -> + printfn "got exception: %s" ae.InnerExceptions.[0].Message +""" + + [] + member _.testTypeInference() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["hello"] """ +let run () : Task = runtimeTask { return "hello" } +let t = run() +t.Wait() +printfn "%s" t.Result +""" + + [] + member _.testBindAllFourTypes() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["3"] """ +// Tests all 4 Bind overloads: Task, Task, ValueTask, ValueTask +let run () : Task = + runtimeTask { + let! a = Task.FromResult(1) + do! Task.CompletedTask + let! b = ValueTask.FromResult(2) + do! ValueTask.CompletedTask + return a + b + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.testZeroAndCombine() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["1"] """ +// Zero is called for the `if true then ()` branch (no else), Combine sequences it with `return 1` +let run () : Task = + runtimeTask { + if true then () + return 1 + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.testDelay() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["1"] """ +// Delay wraps the body in a function; since it's inline, the result is still correct +let mutable x = 0 +let run () : Task = + runtimeTask { + x <- x + 1 + return x + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.testInteropWithTaskCE() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["42"] """ +// runtimeTask can bind the result of a task { } computation expression +let run () : Task = + runtimeTask { + let! x = task { return 42 } + return x + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.testInlineNestedViaSeparateFunctions() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["30"] """ +// Inline-nested runtimeTask CEs work when each nesting level is a separate function. +// True inline nesting (runtimeTask { let! x = runtimeTask { return 10 } }) does NOT work +// because the cast trick only works for the final return of a cil managed async method. +let inner () : Task = + runtimeTask { return 10 } +let middle () : Task = + runtimeTask { + let! x = inner() + return x + 20 + } +let run () : Task = + runtimeTask { + let! y = middle() + return y + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + + [] + member _.testNoMethodImplNeeded() = + Environment.SetEnvironmentVariable("DOTNET_RuntimeAsync", "1") + RuntimeTaskTestHelpers.runTest ["42"] """ +// Consumer functions need NO [] — the [] attribute +// on RuntimeTaskBuilder causes the compiler to automatically emit 'cil managed async'. +let run () : Task = + runtimeTask { + let! x = Task.FromResult(42) + return x + } +let t = run() +t.Wait() +printfn "%d" t.Result +""" + +#endif diff --git a/tests/FSharp.Test.Utilities/Compiler.fs b/tests/FSharp.Test.Utilities/Compiler.fs index 12d22dcac57..588d0f13059 100644 --- a/tests/FSharp.Test.Utilities/Compiler.fs +++ b/tests/FSharp.Test.Utilities/Compiler.fs @@ -1106,6 +1106,35 @@ module rec Compiler = let compileExeAndRun = asExe >> compileAndRun + /// Like run, but executes the compiled app in a new process (inheriting the current process's environment). + /// Dependencies and FSharp.Core are copied to the output directory so the child process can find them. + let runNewProcess (result: CompilationResult) : CompilationResult = + match result with + | CompilationResult.Failure f -> failwith (sprintf "Compilation should be successful in order to run.\n Errors: %A" (f.Diagnostics)) + | CompilationResult.Success s -> + match s.OutputPath with + | None -> failwith "Compilation didn't produce any output. Unable to run. (Did you forget to set output type to Exe?)" + | Some p -> + // Copy dependencies and FSharp.Core to the output directory so the child process can resolve them. + let outputDirectory = System.IO.Path.GetDirectoryName(p) + let copyIfMissing (src: string) = + let destPath = System.IO.Path.Combine(outputDirectory, System.IO.Path.GetFileName(src: string)) + if not (System.IO.File.Exists(destPath)) then + System.IO.File.Copy(src, destPath) + for dep in s.Dependencies do + copyIfMissing dep + // FSharp.Core.dll is referenced via -r: options in defaultProjectOptions but not in s.Dependencies. + // Copy it explicitly so the child process can find it. + let fsharpCoreLocation = typeof.Assembly.Location + copyIfMissing fsharpCoreLocation + let output = CompilerAssert.ExecuteAndReturnResult (p, false, s.Dependencies, true) + let executionResult = { s with Output = Some (ExecutionOutput output) } + match output.Outcome with + | Failure _ -> CompilationResult.Failure executionResult + | _ -> CompilationResult.Success executionResult + + let compileExeAndRunNewProcess = asExe >> compile >> runNewProcess + let private processScriptResults fs (evalResult: Result, err: FSharpDiagnostic[]) outputWritten errorsWritten = let perFileDiagnostics = err |> fromFSharpDiagnostic let diagnostics = perFileDiagnostics |> List.map snd diff --git a/tests/FSharp.Test.Utilities/TestFramework.fs b/tests/FSharp.Test.Utilities/TestFramework.fs index dcb64909ad2..a48cb2cd7e9 100644 --- a/tests/FSharp.Test.Utilities/TestFramework.fs +++ b/tests/FSharp.Test.Utilities/TestFramework.fs @@ -81,6 +81,8 @@ module Commands = // When running tests, we want to roll forward to minor versions (including previews). psi.EnvironmentVariables["DOTNET_ROLL_FORWARD"] <- "LatestMajor" psi.EnvironmentVariables["DOTNET_ROLL_FORWARD_TO_PRERELEASE"] <- "1" + // Enable runtime-async feature for tests that use [] or runtimeTask { }. + psi.EnvironmentVariables["DOTNET_RuntimeAsync"] <- "1" // Host can sometimes add this, and it can break things psi.EnvironmentVariables.Remove("MSBuildSDKsPath")