├── .gitignore ├── Benchmarks ├── Benchmarks.fs ├── Benchmarks.fsproj ├── CS │ ├── Benchmarks.cs │ └── CS.csproj └── Program.fs ├── ComputationBuilders.fs ├── LICENSE.md ├── Ply.fs ├── Ply.fsproj ├── Ply.sln ├── README.md ├── global.json ├── release.sh ├── tests ├── Ply.Tests │ ├── Ply.Tests.fsproj │ ├── Program.fs │ └── Tests.fs └── multi-targetting │ ├── netcoreapp2.1-directdep │ ├── Program.fs │ └── netcoreapp2.1.fsproj │ ├── netcoreapp2.1-ns2.0dep │ ├── Program.fs │ └── netcoreapp2.1.fsproj │ ├── netcoreapp3.1-ns2.1dep │ ├── Program.fs │ └── netcoreapp3.1.fsproj │ ├── netstandard2.0 │ ├── Library.fs │ └── netstandard2.0.fsproj │ ├── netstandard2.1 │ ├── Library.fs │ └── netstandard2.1.fsproj │ └── run.sh └── tools └── icons ├── ply-128x128.png └── ply.svg /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # VS(code), Rider and Xamarin 5 | .vs/ 6 | .vscode/ 7 | .idea/ 8 | *.sln.iml 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | *.userprefs 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | _UpgradeReport_Files/ 19 | Backup*/ 20 | UpgradeLog*.XML 21 | UpgradeLog*.htm 22 | .wercker 23 | 24 | # Build results 25 | [Bb]in/ 26 | [Oo]bj/ 27 | 28 | # dotnet-cli 29 | **/project.lock.json 30 | **/BenchmarkDotNet.Artifacts/* 31 | BDN.Generated 32 | 33 | # ReSharper is a .NET coding add-in 34 | _ReSharper*/ 35 | *.[Rr]e[Ss]harper 36 | *.DotSettings.user 37 | 38 | # JustCode is a .NET coding add-in 39 | .JustCode 40 | 41 | # TeamCity is a build add-in 42 | _TeamCity* 43 | 44 | # Node 45 | node_modules/ -------------------------------------------------------------------------------- /Benchmarks/Benchmarks.fs: -------------------------------------------------------------------------------- 1 | namespace Benchmarks 2 | 3 | open BenchmarkDotNet.Attributes 4 | open System.Threading.Tasks 5 | open FSharp.Control.Tasks.V2 // TaskBuilder.fs 6 | open FSharp.Control.Tasks // Ply 7 | open FSharp.Control.Tasks.Affine.Unsafe // Ply 8 | 9 | [] 10 | [] 11 | type MicroBenchmark() = 12 | [] 13 | member _.AllocFreeReturn() = 14 | for _ = 0 to 100000 do 15 | let ret () = 16 | uply { 17 | return! uvtask { 18 | return! uply { 19 | return 1 20 | } 21 | } 22 | } 23 | ret() |> ignore 24 | 25 | [] 26 | member _.SyncExceptionSuspend() = 27 | for _ = 0 to 1000 do 28 | let ret () = 29 | uply { 30 | return! uvtask { 31 | return! uply { 32 | invalidOp "Will be suspended in an awaitable" 33 | return () 34 | } 35 | } 36 | } 37 | ret() |> ignore 38 | 39 | [] 40 | [] 41 | type TaskBuildersBenchmark() = 42 | let oldTask = ContextSensitive.task 43 | 44 | let arbitraryWork(work) = CS.Benchmarks.ArbitraryWork(work) 45 | 46 | // Keep at 200 minimum otherwise the C# version will do an await while the F# version 47 | // gets IsCompleted true due to a few more calls in between running and checking 48 | let workFactor = 200 49 | let loopCount = 100000 50 | 51 | [] 52 | member _.TaskBuilderOpt () = 53 | (task { 54 | do! Task.Yield() 55 | let! arb = Task.Run(arbitraryWork workFactor) 56 | let! v = task { 57 | return! ValueTask<_>(arb) 58 | } 59 | 60 | let mutable i = loopCount 61 | while i > 0 do 62 | if i % 2 = 0 then 63 | let! y = Task.Run(arbitraryWork workFactor).ConfigureAwait(false) 64 | () 65 | i <- i - 1 66 | if v > 0 then return! ValueTask<_>(v) else return 0 67 | }).Result 68 | 69 | [] 70 | member _.TaskBuilder () = 71 | (oldTask { 72 | do! Task.Yield() 73 | let! arb = Task.Run(arbitraryWork workFactor) 74 | let! v = oldTask { 75 | return! ValueTask<_>(arb) 76 | } 77 | 78 | let mutable i = loopCount 79 | while i > 0 do 80 | if i % 2 = 0 then 81 | let! y = Task.Run(arbitraryWork workFactor).ConfigureAwait(false) 82 | () 83 | i <- i - 1 84 | if v > 0 then return! ValueTask<_>(v) else return 0 85 | }).Result 86 | 87 | [] 88 | member _.CSAsyncAwait () = 89 | CS.Benchmarks.CsTasks(workFactor, loopCount).Result 90 | -------------------------------------------------------------------------------- /Benchmarks/Benchmarks.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Benchmarks/CS/Benchmarks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | 6 | namespace CS 7 | { 8 | public class Benchmarks 9 | { 10 | [MethodImpl(MethodImplOptions.NoInlining)] 11 | public static Func ArbitraryWork(int work) 12 | { 13 | return () => Enumerable.Range(0, work).Sum(); 14 | } 15 | 16 | public static async ValueTask CsTasks(int workFactor, int loopCount) 17 | { 18 | await Task.Yield(); 19 | var arb = await Task.Run(ArbitraryWork(workFactor)); 20 | async Task Func() => await new ValueTask(arb); 21 | var v = await Func(); 22 | 23 | var i = loopCount; 24 | while (i > 0) 25 | { 26 | if (i % 2 == 0) 27 | await Task.Run(ArbitraryWork(workFactor)).ConfigureAwait(false); 28 | i = i - 1; 29 | } 30 | 31 | if (v > 0) 32 | { 33 | return await new ValueTask(v); 34 | } 35 | else 36 | { 37 | return 0; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Benchmarks/CS/CS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Benchmarks/Program.fs: -------------------------------------------------------------------------------- 1 | module Ply.Benchmarks 2 | 3 | open System.Reflection 4 | open BenchmarkDotNet.Running 5 | 6 | type private This = 7 | class 8 | end 9 | 10 | [] 11 | let main argv = 12 | let benchmarks = BenchmarkSwitcher.FromAssembly(typeof.GetTypeInfo().Assembly) 13 | benchmarks.Run(argv) |> ignore 14 | 0 // return an integer exit code 15 | -------------------------------------------------------------------------------- /ComputationBuilders.fs: -------------------------------------------------------------------------------- 1 | // Optimized (Value)Task computation expressions for F# 2 | // Author: Nino Floris - mail@ninofloris.com 3 | // Copyright (c) 2019 Crowded B.V. 4 | // Distributed under the MIT License (https://opensource.org/licenses/MIT). 5 | 6 | namespace FSharp.Control.Tasks 7 | 8 | open System 9 | open System.ComponentModel 10 | open Ply 11 | open Ply.TplPrimitives 12 | open System.Threading.Tasks 13 | 14 | [] 15 | module Builders = 16 | type TaskBuilder() = 17 | inherit AffineBuilder() 18 | member inline __.Run(f : unit -> Ply<'u>) : Task<'u> = runAsTask f 19 | 20 | type UnitTaskBuilder() = 21 | inherit AffineBuilder() 22 | member inline __.Run(f : unit -> Ply<'u>) = 23 | let t = run f 24 | if t.IsCompletedSuccessfully then Task.CompletedTask else t.AsTask() :> Task 25 | 26 | type ValueTaskBuilder() = 27 | inherit AffineBuilder() 28 | member inline __.Run(f : unit -> Ply<'u>) = run f 29 | 30 | type UnitValueTaskBuilder() = 31 | inherit AffineBuilder() 32 | member inline __.Run(f : unit -> Ply<'u>) = 33 | let t = run f 34 | if t.IsCompletedSuccessfully then ValueTask() else ValueTask(t.AsTask() :> Task) 35 | 36 | // Backwards compat. 37 | [] 38 | let task = TaskBuilder() 39 | [] 40 | let unitTask = UnitTaskBuilder() 41 | [] 42 | let vtask = ValueTaskBuilder() 43 | [] 44 | let unitVtask = UnitValueTaskBuilder() 45 | 46 | module Unsafe = 47 | type UnsafePlyBuilder() = 48 | inherit AffineBuilder() 49 | member inline __.Run(f : unit -> Ply<'u>) = runUnwrappedAsPly f 50 | 51 | type UnsafeUnitTaskBuilder() = 52 | inherit AffineBuilder() 53 | member inline __.Run(f : unit -> Ply<'u>) = 54 | let t = runUnwrapped f 55 | if t.IsCompletedSuccessfully then Task.CompletedTask else t.AsTask() :> Task 56 | 57 | type UnsafeValueTaskBuilder() = 58 | inherit AffineBuilder() 59 | member inline __.Run(f : unit -> Ply<'u>) = runUnwrapped f 60 | 61 | type UnsafeUnitValueTaskBuilder() = 62 | inherit AffineBuilder() 63 | member inline __.Run(f : unit -> Ply<'u>) = 64 | let t = runUnwrapped f 65 | if t.IsCompletedSuccessfully then ValueTask() else ValueTask(t.AsTask() :> Task) 66 | 67 | // Backwards compat. 68 | [] 69 | let uply = UnsafePlyBuilder() 70 | [] 71 | let uunitTask = UnsafeUnitTaskBuilder() 72 | [] 73 | let uvtask = UnsafeValueTaskBuilder() 74 | [] 75 | let uunitVtask = UnsafeUnitValueTaskBuilder() 76 | 77 | module NonAffine = 78 | type TaskBuilder() = 79 | inherit NonAffineBuilder() 80 | member inline __.Run(f : unit -> Ply<'u>) : Task<'u> = runAsTask f 81 | 82 | type UnitTaskBuilder() = 83 | inherit NonAffineBuilder() 84 | member inline __.Run(f : unit -> Ply<'u>) = 85 | let t = run f 86 | if t.IsCompletedSuccessfully then Task.CompletedTask else t.AsTask() :> Task 87 | 88 | type ValueTaskBuilder() = 89 | inherit NonAffineBuilder() 90 | member inline __.Run(f : unit -> Ply<'u>) = run f 91 | 92 | type UnitValueTaskBuilder() = 93 | inherit NonAffineBuilder() 94 | member inline __.Run(f : unit -> Ply<'u>) = 95 | let t = run f 96 | if t.IsCompletedSuccessfully then ValueTask() else ValueTask(t.AsTask() :> Task) 97 | 98 | module Unsafe = 99 | type UnsafePlyBuilder() = 100 | inherit NonAffineBuilder() 101 | member inline __.Run(f : unit -> Ply<'u>) = runUnwrappedAsPly f 102 | 103 | type UnsafeUnitTaskBuilder() = 104 | inherit NonAffineBuilder() 105 | member inline __.Run(f : unit -> Ply<'u>) = 106 | let t = runUnwrapped f 107 | if t.IsCompletedSuccessfully then Task.CompletedTask else t.AsTask() :> Task 108 | 109 | type UnsafeValueTaskBuilder() = 110 | inherit NonAffineBuilder() 111 | member inline __.Run(f : unit -> Ply<'u>) = runUnwrapped f 112 | 113 | type UnsafeUnitValueTaskBuilder() = 114 | inherit NonAffineBuilder() 115 | member inline __.Run(f : unit -> Ply<'u>) = 116 | let t = runUnwrapped f 117 | if t.IsCompletedSuccessfully then ValueTask() else ValueTask(t.AsTask() :> Task) 118 | 119 | [] 120 | /// Defines builders that are scheduler affine, respecting the SynchronizationContext or current TaskScheduler. 121 | /// These match C# async await behavior, when building an application you normally want to use these builders. 122 | module Affine = 123 | open Builders 124 | 125 | let task = TaskBuilder() 126 | let unitTask = UnitTaskBuilder() 127 | let vtask = ValueTaskBuilder() 128 | let unitVtask = UnitValueTaskBuilder() 129 | 130 | module Unsafe = 131 | open Unsafe 132 | let uply = UnsafePlyBuilder() 133 | let uunitTask = UnsafeUnitTaskBuilder() 134 | let uvtask = UnsafeValueTaskBuilder() 135 | let uunitVtask = UnsafeUnitValueTaskBuilder() 136 | 137 | /// Defines builders that are free of scheduler affinity, rejecting the SynchronizationContext or current TaskScheduler. 138 | /// Also known as Task.ConfigureAwait(false), when building a library you want to use these builders. 139 | module NonAffine = 140 | open Builders.NonAffine 141 | 142 | let task = TaskBuilder() 143 | let unitTask = UnitTaskBuilder() 144 | let vtask = ValueTaskBuilder() 145 | let unitVtask = UnitValueTaskBuilder() 146 | 147 | module Unsafe = 148 | open Unsafe 149 | let uply = UnsafePlyBuilder() 150 | let uunitTask = UnsafeUnitTaskBuilder() 151 | let uvtask = UnsafeValueTaskBuilder() 152 | let uunitVtask = UnsafeUnitValueTaskBuilder() 153 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2019 - present Crowded B.V. 5 | 6 | All rights reserved. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /Ply.fs: -------------------------------------------------------------------------------- 1 | // Optimized (Value)Task computation expressions for F# 2 | // Author: Nino Floris - mail@ninofloris.com 3 | // Copyright (c) 2019 Crowded B.V. 4 | // Distributed under the MIT License (https://opensource.org/licenses/MIT). 5 | 6 | #if NETSTANDARD2_0 7 | namespace System.Runtime.CompilerServices 8 | [] 9 | type IsReadOnlyAttribute() = 10 | inherit System.Attribute() 11 | #endif 12 | 13 | namespace rec Ply 14 | open System 15 | open System.Runtime.CompilerServices 16 | open System.Runtime.InteropServices 17 | open System.Threading.Tasks 18 | open System.Runtime.ExceptionServices 19 | 20 | #nowarn "1204" 21 | 22 | module internal Internal = 23 | type [] IAwaitable<'u> = 24 | abstract member Await : machine: byref<#IAwaitingMachine> -> unit 25 | abstract member Continuation : (unit -> Ply<'u>) 26 | and IAwaitingMachine = 27 | abstract member AwaitUnsafeOnCompleted<'awt when 'awt :> ICriticalNotifyCompletion> : awt: byref<'awt> -> unit 28 | 29 | type [] Ply<'u> = 30 | val private value : 'u 31 | val private awaitable : Internal.IAwaitable<'u> 32 | new(result: 'u) = { value = result; awaitable = Unchecked.defaultof<_>; } 33 | internal new(await) = { value = Unchecked.defaultof<_>; awaitable = await } 34 | member this.IsCompletedSuccessfully = isNull this.awaitable 35 | member internal this.Awaitable = this.awaitable 36 | member internal this.Continuation = this.awaitable.Continuation 37 | member this.Result = if this.IsCompletedSuccessfully then this.value else this.Continuation().Result 38 | 39 | [] 40 | /// Entrypoint for generated code 41 | module TplPrimitives = 42 | open Internal 43 | 44 | let inline createBuilder() = 45 | AsyncValueTaskMethodBuilder<_>() 46 | 47 | let inline defaultof<'T> = Unchecked.defaultof<'T> 48 | let inline unbox<'T> (x: obj) : 'T = LanguagePrimitives.IntrinsicFunctions.UnboxFast x 49 | let ret x = Ply(result = x) 50 | let zero = ret () 51 | 52 | type ExceptionDispatchInfo with 53 | member inline x.Raise() = x.Throw(); defaultof<_> 54 | 55 | // https://github.com/dotnet/coreclr/pull/15781/files 56 | type [] internal ContinuationStateMachine<'u> = 57 | val Builder : AsyncValueTaskMethodBuilder<'u> 58 | val mutable private continuation: unit -> Ply<'u> 59 | 60 | new(awaitable) = { Builder = createBuilder(); continuation = fun () -> Ply(await = awaitable) } 61 | new(continuation) = { Builder = createBuilder(); continuation = continuation } 62 | 63 | interface IAwaitingMachine with 64 | member this.AwaitUnsafeOnCompleted(awt: byref<'awt>) = 65 | this.Builder.AwaitUnsafeOnCompleted(&awt, &this) 66 | 67 | interface IAsyncStateMachine with 68 | // This method is effectively deprecated on .NET Core so only .NET Fx will still call this. 69 | member this.SetStateMachine(csm) = 70 | this.Builder.SetStateMachine(csm) 71 | 72 | member this.MoveNext() = 73 | let mutable ex = null 74 | try 75 | let next = this.continuation() 76 | if next.IsCompletedSuccessfully then 77 | this.Builder.SetResult(next.Result) 78 | else 79 | this.continuation <- next.Continuation 80 | next.Awaitable.Await(&this) 81 | with exn -> ex <- exn 82 | 83 | if not (isNull ex) then this.Builder.SetException(ex) 84 | 85 | and [] internal TplAwaitable<'awt, 'u when 'awt :> ICriticalNotifyCompletion>(awaiter: 'awt, cont: unit -> Ply<'u>) = 86 | let mutable awaiter = awaiter 87 | interface IAwaitable<'u> with 88 | member _.Await(csm) = csm.AwaitUnsafeOnCompleted(&awaiter) 89 | member _.Continuation = cont 90 | 91 | and [] NoEdiFSharpFunc<'t,'u>() = 92 | inherit FSharpFunc<'t,'u>() 93 | // Removes "--- End of stack trace from previous location where exception was thrown ---" 94 | // from implementers that catch exceptions for the purpose of passing them along as Edi. 95 | // See https://github.com/dotnet/coreclr/pull/15781/files 96 | interface IAsyncStateMachine with 97 | member _.SetStateMachine csm = failwith "not implemented" 98 | member _.MoveNext() = failwith "not implemented" 99 | 100 | // Unfortunate to have two almost identical awaitables; combining them either makes for poor stack traces or worse perf. 101 | and [] internal PlyAwaitable<'t,'u>(awaitable: IAwaitable<'t>, cont: Result<'t, ExceptionDispatchInfo> -> Ply<'u>) = 102 | inherit NoEdiFSharpFunc>() 103 | let mutable awaitable = awaitable 104 | 105 | override this.Invoke r = 106 | let mutable (next: Ply<'t>, edi: ExceptionDispatchInfo) = defaultof<_>, null 107 | // Make sure we run the inner continuation alone in this try block. 108 | try next <- awaitable.Continuation() 109 | with ex -> edi <- ExceptionDispatchInfo.Capture(ex) 110 | 111 | if isNull edi then 112 | if next.IsCompletedSuccessfully then 113 | cont(Ok next.Result) 114 | else 115 | awaitable <- next.Awaitable 116 | Ply<_>(await = this) 117 | else 118 | cont(Error edi) 119 | 120 | interface IAwaitable<'u> with 121 | member _.Await(csm) = awaitable.Await(&csm) 122 | // `:> obj` here is critical to preventing fsc from generating an FSharpFunc wrapping ours for some reason. 123 | member this.Continuation = this :> obj |> unbox<_> 124 | 125 | and [] internal AwaitableContinuation<'s,'t,'u>(state: 's, ply: Ply<'t>, cont: AwaitableContinuation<'s,'t,'u> -> Ply<'u>) = 126 | inherit NoEdiFSharpFunc>() 127 | let mutable ply, exceptionDispatchInfo = ply, defaultof<_> 128 | override this.Invoke r = 129 | // See if we're created with a completed ply result, sometimes happens when we need result suspension (like ediPly) 130 | if ply.IsCompletedSuccessfully then 131 | cont this 132 | else 133 | 134 | let mutable (next: Ply<'t>, edi: ExceptionDispatchInfo) = defaultof<_>, null 135 | // Make sure we run the inner continuation alone in this try block. 136 | try next <- ply.Awaitable.Continuation() 137 | with ex -> edi <- ExceptionDispatchInfo.Capture(ex) 138 | 139 | if isNull edi then 140 | if next.IsCompletedSuccessfully then 141 | ply <- Ply<_>(result = next.Result) 142 | cont this 143 | else 144 | ply <- next; Ply(await = this) 145 | else 146 | exceptionDispatchInfo <- edi 147 | cont this 148 | 149 | // For iterative continuations 150 | member this.SetAwaitable(next) = ply <- next 151 | 152 | member this.State = state 153 | member this.Value = ply 154 | member this.IsCompletedSuccessfully = this.Value.IsCompletedSuccessfully 155 | member this.Edi = exceptionDispatchInfo 156 | 157 | interface IAwaitable<'u> with 158 | member this.Await(csm) = 159 | // See if we're created with a completed ply result, sometimes happens when we need result suspension (like ediPly) 160 | // Yielding here is not great but the other option is re-introducing a hasYielded bool return value so MoveNext knows it should not unwind. 161 | // As this is rarely used - uply users that synchronously complete with an exception - the tradeoff is worth the cost of a dispatch. 162 | if ply.IsCompletedSuccessfully then 163 | let mutable awt = Task.Yield().GetAwaiter() in csm.AwaitUnsafeOnCompleted(&awt) 164 | else 165 | ply.Awaitable.Await(&csm) 166 | // `:> obj` here is critical to preventing fsc from generating an FSharpFunc wrapping ours for some reason. 167 | member this.Continuation = this :> obj |> unbox<_> 168 | 169 | // Not inlined to protect implementation details 170 | let ediPly (edi: ExceptionDispatchInfo) = 171 | Ply(await = (AwaitableContinuation(edi, Ply<_>(result = edi), fun this -> this.Value.Result.Raise()))) 172 | 173 | // Runs any continuation directly, without any execution context capture, but still suspending any exceptions. 174 | // Exceptions outside a builder can happen here during Bind when an awaiter is completed but GetResult throws. 175 | let inline runUnwrappedAsPly (f: unit -> Ply<'u>) : Ply<'u> = 176 | try f() with ex -> ediPly (ExceptionDispatchInfo.Capture ex) 177 | 178 | let run (f: unit -> Ply<'u>) : ValueTask<'u> = 179 | // ContinuationStateMachine contains a mutable struct so we need to prevent struct copies. 180 | let mutable x = ContinuationStateMachine<_>(f) 181 | x.Builder.Start(&x) 182 | x.Builder.Task 183 | 184 | let runPly (ply: Ply<'u>) : ValueTask<'u> = 185 | if ply.IsCompletedSuccessfully then 186 | let mutable b = createBuilder() 187 | b.SetResult(ply.Result) 188 | b.Task 189 | else 190 | let mutable x = ContinuationStateMachine<_>(ply.Awaitable) 191 | x.Builder.Start(&x) 192 | x.Builder.Task 193 | 194 | // This won't correctly prevent AsyncLocal leakage or SyncContext switches but it does save us the closure alloc 195 | // Making only this version completely alloc free for the fast path... 196 | // Read more here https://github.com/dotnet/coreclr/blob/027a9105/src/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilder.cs#L954 197 | let inline runUnwrapped (f: unit -> Ply<'u>) : ValueTask<'u> = 198 | let next = runUnwrappedAsPly f 199 | if next.IsCompletedSuccessfully then 200 | let mutable b = createBuilder() 201 | b.SetResult(next.Result) 202 | b.Task 203 | else 204 | runPly next 205 | 206 | let runAsTask (f: unit -> Ply<'u>) : Task<'u> = 207 | // ContinuationStateMachine contains a mutable struct so we need to prevent struct copies. 208 | let mutable x = ContinuationStateMachine<_>(f) 209 | x.Builder.Start(&x) 210 | x.Builder.Task.AsTask() 211 | 212 | let runPlyAsTask (ply: Ply<'u>) : Task<'u> = 213 | let task = 214 | if ply.IsCompletedSuccessfully then 215 | let mutable b = createBuilder() 216 | b.SetResult(ply.Result) 217 | b.Task 218 | else 219 | let mutable x = ContinuationStateMachine<_>(ply.Awaitable) 220 | x.Builder.Start(&x) 221 | x.Builder.Task 222 | 223 | task.AsTask() 224 | 225 | // This won't correctly prevent AsyncLocal leakage or SyncContext switches but it does save us the closure alloc 226 | // Making only this version completely alloc free for the fast path... 227 | // Read more here https://github.com/dotnet/coreclr/blob/027a9105/src/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilder.cs#L954 228 | let inline runUnwrappedAsTask (f: unit -> Ply<'u>) : Task<'u> = 229 | let next = runUnwrappedAsPly f 230 | if next.IsCompletedSuccessfully then 231 | let mutable b = createBuilder() 232 | b.SetResult(next.Result) 233 | b.Task.AsTask() 234 | else 235 | runPlyAsTask next 236 | 237 | let combine (ply : Ply) (continuation : unit -> Ply<'u>) = 238 | if ply.IsCompletedSuccessfully then continuation() 239 | else 240 | Ply(await = (AwaitableContinuation(continuation, ply, fun this -> if this.IsCompletedSuccessfully then this.State() else this.Edi.Raise()))) 241 | 242 | let tryWith(continuation : unit -> Ply<'u>) (catch : exn -> Ply<'u>) = 243 | try 244 | let next = continuation() 245 | if next.IsCompletedSuccessfully then 246 | next 247 | else 248 | Ply(await = (AwaitableContinuation(catch, next, fun this -> if this.IsCompletedSuccessfully then this.Value else this.State this.Edi.SourceException))) 249 | with ex -> catch ex 250 | 251 | let rec tryFinally (continuation : unit -> Ply<'u>) (finallyBody : unit -> unit) = 252 | try 253 | let next = continuation() 254 | if next.IsCompletedSuccessfully then 255 | finallyBody(); next 256 | else 257 | Ply(await = (AwaitableContinuation(finallyBody, next, fun this -> this.State(); if this.IsCompletedSuccessfully then this.Value else this.Edi.Raise()))) 258 | with ex -> 259 | finallyBody() 260 | reraise() 261 | 262 | let rec whileLoop (cond : unit -> bool) (body : unit -> Ply) = 263 | // As long as we never yield loops are allocation free 264 | if cond() then 265 | let next = body() 266 | if next.IsCompletedSuccessfully then 267 | whileLoop cond body 268 | else 269 | let cont = AwaitableContinuation(struct(cond,body), next, fun this -> 270 | // Every resumption we go through could end in a stored exception 271 | if not this.IsCompletedSuccessfully then this.Edi.Raise() 272 | let struct(cond, body) = this.State 273 | let mutable awaitable = zero 274 | while awaitable.IsCompletedSuccessfully && cond() do 275 | let next = body() 276 | if not next.IsCompletedSuccessfully then 277 | this.SetAwaitable(next) 278 | awaitable <- Ply(await = this) 279 | awaitable) 280 | Ply(await = cont) 281 | else zero 282 | 283 | let inline using (disposable : #IDisposable) (body : #IDisposable -> Ply<'u>) = 284 | tryFinally (fun () -> body disposable) (fun () -> if not(isNull(disposable :> obj)) then disposable.Dispose()) 285 | 286 | let inline forLoop (sequence : 'a seq) (body : 'a -> Ply) = 287 | using (sequence.GetEnumerator()) (fun e -> whileLoop e.MoveNext (fun () -> body e.Current)) 288 | 289 | // These types exist for backwards compatibility until 1.0 290 | // Some types here are supposed to always be instantiated at unit see https://github.com/dotnet/fsharp/issues/9913 291 | type IAwaiterMethods<'awt, 'res when 'awt :> ICriticalNotifyCompletion> = 292 | abstract member IsCompleted: byref<'awt> -> bool 293 | abstract member GetResult: byref<'awt> -> 'res 294 | 295 | type []TaskAwaiterMethods<'t> = 296 | interface IAwaiterMethods, 't> with 297 | member __.IsCompleted awt = awt.IsCompleted 298 | member __.GetResult awt = awt.GetResult() 299 | and []UnitTaskAwaiterMethods<'t> = 300 | interface IAwaiterMethods with 301 | member __.IsCompleted awt = awt.IsCompleted 302 | member __.GetResult awt = awt.GetResult(); defaultof<_> // Always unit 303 | 304 | and []ConfiguredTaskAwaiterMethods<'t> = 305 | interface IAwaiterMethods.ConfiguredTaskAwaiter, 't> with 306 | member __.IsCompleted awt = awt.IsCompleted 307 | member __.GetResult awt = awt.GetResult() 308 | and []ConfiguredUnitTaskAwaiterMethods<'t> = 309 | interface IAwaiterMethods with 310 | member __.IsCompleted awt = awt.IsCompleted 311 | member __.GetResult awt = awt.GetResult(); defaultof<_> // Always unit 312 | 313 | and []YieldAwaiterMethods<'t> = 314 | interface IAwaiterMethods with 315 | member __.IsCompleted awt = awt.IsCompleted 316 | member __.GetResult awt = awt.GetResult(); defaultof<_> // Always unit 317 | 318 | and []GenericAwaiterMethods<'awt, 't when 'awt :> ICriticalNotifyCompletion> = 319 | interface IAwaiterMethods<'awt, 't> with 320 | member __.IsCompleted awt = false // Always await, this way we don't have to specialize per awaiter 321 | member __.GetResult awt = defaultof<_> // Always unit because we wrap this continuation to always be unit -> Ply<'u> 322 | 323 | and []ValueTaskAwaiterMethods<'t> = 324 | interface IAwaiterMethods, 't> with 325 | member __.IsCompleted awt = awt.IsCompleted 326 | member __.GetResult awt = awt.GetResult() 327 | and []UnitValueTaskAwaiterMethods<'t> = 328 | interface IAwaiterMethods with 329 | member __.IsCompleted awt = awt.IsCompleted 330 | member __.GetResult awt = awt.GetResult(); defaultof<_> // Always unit 331 | 332 | and []ConfiguredValueTaskAwaiterMethods<'t> = 333 | interface IAwaiterMethods.ConfiguredValueTaskAwaiter, 't> with 334 | member __.IsCompleted awt = awt.IsCompleted 335 | member __.GetResult awt = awt.GetResult() 336 | and []ConfiguredUnitValueTaskAwaiterMethods<'t> = 337 | interface IAwaiterMethods with 338 | member __.IsCompleted awt = awt.IsCompleted 339 | member __.GetResult awt = awt.GetResult(); defaultof<_> // Always unit 340 | 341 | type Binder<'u>() = 342 | // Exists for binary compatibility reasons until 1.0 343 | static member Await<'methods, 'awt, 't when 'methods :> IAwaiterMethods<'awt, 't>>(awt: byref<'awt>, cont: 't -> Ply<'u>) = 344 | let awt = awt in Ply(await = TplAwaitable<_,_>(awt, fun () -> let mutable awt = awt in cont (defaultof<'methods>.GetResult(&awt)))) 345 | 346 | // We keep Await non inline to protect internals for maximum binary compatibility. 347 | static member Await<'awt, 't when 'awt :> ICriticalNotifyCompletion>(awt: byref<'awt>, cont: unit -> Ply<'u>) = 348 | Ply(await = TplAwaitable<_,_>(awt,cont)) 349 | 350 | static member inline Tpl< ^awt, 't 351 | when ^awt :> ICriticalNotifyCompletion 352 | and ^awt : (member get_IsCompleted: unit -> bool) 353 | and ^awt : (member GetResult: unit -> 't) > 354 | (awt: ^awt, cont: 't -> Ply<'u>) = 355 | if (^awt : (member get_IsCompleted: unit -> bool) (awt)) then 356 | cont (^awt : (member GetResult: unit -> 't) (awt)) 357 | else 358 | // Having GetResult here means user stack frames will get captured, as this code will get inlined into cont. 359 | let mutable mutAwt = awt in Binder<'u>.Await<_,_>(&mutAwt, fun () -> cont (^awt : (member GetResult : unit -> 't) (awt))) 360 | 361 | // Exists for binary compatibility reasons until 1.0 362 | static member PlyAwait(ply: Ply<'t>, cont: 't -> Ply<'u>) = 363 | Ply(await = (AwaitableContinuation(cont, ply, fun this -> if this.IsCompletedSuccessfully then this.State this.Value.Result else this.Edi.Raise()))) 364 | 365 | static member PlyAwait(ply: Ply<'t>, resultCont: Result<'t, ExceptionDispatchInfo> -> Ply<'u>) = 366 | Ply(await = (PlyAwaitable(ply.Awaitable, resultCont))) 367 | 368 | static member inline Ply(ply: Ply<'t>, cont: 't -> Ply<'u>) = 369 | if ply.IsCompletedSuccessfully then cont ply.Result 370 | else Binder.PlyAwait(ply, fun (r: Result<'t, ExceptionDispatchInfo>) -> match r with Ok v -> cont v | Error edi -> edi.Raise()) 371 | 372 | // Supporting types to have the compiler do what we want with respect to overload resolution. 373 | type Id<'t> = class end 374 | type Default3() = class end 375 | type Default2() = inherit Default3() 376 | type Default1() = inherit Default2() 377 | 378 | type Bind() = 379 | inherit Default1() 380 | 381 | static member inline Invoke (m, taskLike, cont: 't -> Ply<'u>) = 382 | let inline call_2 (task: ^b, cont, a: ^a) = ((^a or ^b) : (static member Bind : _*_*_ -> Ply<'u>) task, cont, a) 383 | let inline call (task: 'b, cont, a: 'a) = call_2 (task, cont, a) 384 | call(taskLike, cont, m) 385 | 386 | static member inline Bind(configuredTask: ConfiguredTaskAwaitable<'t>, cont: 't -> Ply<'u>, []_impl:Default1) = 387 | Binder<'u>.Tpl<_,_>(configuredTask.GetAwaiter(), cont) 388 | 389 | static member inline Bind(configuredUnitTask: ConfiguredTaskAwaitable, cont: unit -> Ply<'u>, []_impl:Default1) = 390 | Binder<'u>.Tpl<_,_>(configuredUnitTask.GetAwaiter(), cont) 391 | 392 | static member inline Bind(yieldAwaitable: YieldAwaitable, cont: unit -> Ply<'u>, []_impl:Default1) = 393 | Binder<'u>.Tpl<_,_>(yieldAwaitable.GetAwaiter(), cont) 394 | 395 | static member inline Bind(_: Id<'t>, _: 't -> Ply<'u>, []_impl:Default1) = 396 | failwith "Used for forcing delayed resolution." 397 | 398 | static member inline Bind(configuredValueTask: ConfiguredValueTaskAwaitable<'t>, cont: 't -> Ply<'u>, []_impl:Default1) = 399 | Binder<'u>.Tpl<_,_>(configuredValueTask.GetAwaiter(), cont) 400 | 401 | static member inline Bind(configuredUnitValueTask: ConfiguredValueTaskAwaitable, cont: unit -> Ply<'u>, []_impl:Default1) = 402 | Binder<'u>.Tpl<_,_>(configuredUnitValueTask.GetAwaiter(), cont) 403 | 404 | static member inline Bind(ply: Ply<'t>, cont: 't -> Ply<'u>, []_impl:Bind) = 405 | Binder<'u>.Ply(ply, cont) 406 | 407 | type AffineBind() = 408 | inherit Bind() 409 | 410 | static member inline Bind(taskLike: ^taskLike, cont: 't -> Ply<'u>, []_impl:Default3) = 411 | Binder<'u>.Tpl<_,_>((^taskLike : (member GetAwaiter: unit -> ^awt) (taskLike)), cont) 412 | 413 | static member inline Bind(unitTask: Task, cont: unit -> Ply<'u>, []_impl:Default2) = 414 | Binder<'u>.Tpl<_,_>(unitTask.GetAwaiter(), cont) 415 | 416 | static member inline Bind(task: Task<'t>, cont: 't -> Ply<'u>, []_impl:Default1) = 417 | Binder<'u>.Tpl<_,_>(task.GetAwaiter(), cont) 418 | 419 | static member inline Bind(async: Async<'t>, cont: 't -> Ply<'u>, []_impl:Default1) = 420 | Binder<'u>.Tpl<_,_>((Async.StartAsTask async).GetAwaiter(), cont) 421 | 422 | static member inline Bind(valueTask: ValueTask<'t>, cont: 't -> Ply<'u>, []_impl:Default1) = 423 | Binder<'u>.Tpl<_,_>(valueTask.GetAwaiter(), cont) 424 | 425 | static member inline Bind(unitValueTask: ValueTask, cont: unit -> Ply<'u>, []_impl:Default1) = 426 | Binder<'u>.Tpl<_,_>(unitValueTask.GetAwaiter(), cont) 427 | 428 | type NonAffineBind() = 429 | inherit Bind() 430 | 431 | static member inline Bind(taskLike: ^taskLike, cont: 't -> Ply<'u>, []_impl:Default3) = 432 | Binder<'u>.Tpl<_,_>((^taskLike : (member GetAwaiter: unit -> ^awt) (taskLike)), cont) 433 | 434 | static member inline Bind(taskLike: ^taskLike, cont: 't -> Ply<'u>, []_impl:Default3) = 435 | let configured = (^taskLike : (member ConfigureAwait : bool -> ^awaitable)(taskLike, false)) 436 | Binder<'u>.Tpl<_,_>((^awaitable : (member GetAwaiter: unit -> ^awt) (configured)), cont) 437 | 438 | static member inline Bind(unitTask: Task, cont: unit -> Ply<'u>, []_impl:Default2) = 439 | Binder<'u>.Tpl<_,_>(unitTask.ConfigureAwait(false).GetAwaiter(), cont) 440 | 441 | static member inline Bind(task: Task<'t>, cont: 't -> Ply<'u>, []_impl:Default1) = 442 | Binder<'u>.Tpl<_,_>(task.ConfigureAwait(false).GetAwaiter(), cont) 443 | 444 | static member inline Bind(async: Async<'t>, cont: 't -> Ply<'u>, []_impl:Default1) = 445 | Binder<'u>.Tpl<_,_>((Async.StartAsTask async).ConfigureAwait(false).GetAwaiter(), cont) 446 | 447 | static member inline Bind(valueTask: ValueTask<'t>, cont: 't -> Ply<'u>, []_impl:Default1) = 448 | Binder<'u>.Tpl<_,_>(valueTask.ConfigureAwait(false).GetAwaiter(), cont) 449 | 450 | static member inline Bind(unitValueTask: ValueTask, cont: unit -> Ply<'u>, []_impl:Default1) = 451 | Binder<'u>.Tpl<_,_>(unitValueTask.ConfigureAwait(false).GetAwaiter(), cont) 452 | 453 | type AwaitableBuilder() = 454 | member inline __.Delay(body : unit -> Ply<'t>) = body 455 | member inline __.Return(x) = ret x 456 | member inline __.Zero() = zero 457 | member inline __.Combine(ply : Ply, continuation: unit -> Ply<'u>) = combine ply continuation 458 | member inline __.While(condition : unit -> bool, body : unit -> Ply) = whileLoop condition body 459 | member inline __.TryWith(body : unit -> Ply<'t>, catch : exn -> Ply<'t>) = tryWith body catch 460 | member inline __.TryFinally(body : unit -> Ply<'t>, finallyBody : unit -> unit) = tryFinally body finallyBody 461 | member inline __.Using(disposable : #IDisposable, body : #IDisposable -> Ply<'u>) = using disposable body 462 | member inline __.For(sequence : seq<_>, body : _ -> Ply) = forLoop sequence body 463 | 464 | type AffineBuilder() = 465 | inherit AwaitableBuilder() 466 | member inline __.ReturnFrom(task: ^taskLike) = Bind.Invoke(defaultof, task, ret) 467 | member inline __.Bind(task: ^taskLike, continuation: 't -> Ply<'u>) = Bind.Invoke(defaultof, task, continuation) 468 | 469 | type NonAffineBuilder() = 470 | inherit AwaitableBuilder() 471 | member inline __.ReturnFrom(task: ^taskLike) = Bind.Invoke(defaultof, task, ret) 472 | member inline __.Bind(task: ^taskLike, continuation: 't -> Ply<'u>) = Bind.Invoke(defaultof, task, continuation) 473 | -------------------------------------------------------------------------------- /Ply.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Ply 6 | 0.3.1 7 | A high performance TPL library for F#. 8 | Copyright 2019 Crowded B.V. 9 | Nino Floris and contributors 10 | en-US 11 | 12 | 13 | netstandard2.0 14 | portable 15 | Library 16 | true 17 | false 18 | true 19 | 20 | 21 | Ply 22 | Ply;Tasks;TPL;F#;FSharp;Functional;Performance 23 | https://github.com/crowded/ply 24 | git 25 | https://github.com/crowded/ply 26 | https://raw.githubusercontent.com/crowded/ply/master/RELEASE_NOTES.md 27 | https://raw.githubusercontent.com/crowded/ply/master/tools/icons/ply-128x128.png 28 | ply-128x128.png 29 | MIT 30 | true 31 | 32 | 33 | true 34 | true 35 | 36 | false 37 | $(EnableSourceLink) 38 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 39 | FS2003;FS0044 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Ply.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Ply", "Ply.fsproj", "{61EE2786-BFEA-45D0-8AF7-BD76DE47701B}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Benchmarks", "Benchmarks\Benchmarks.fsproj", "{975E9B88-80E4-4925-819F-55663ACCC7D4}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CS", "Benchmarks\CS\CS.csproj", "{4146B8F5-27B5-493D-9F74-4AA530469562}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{3799082F-4B66-419A-82B7-051BAD304718}" 13 | EndProject 14 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Ply.Tests", "tests\Ply.Tests\Ply.Tests.fsproj", "{918E5168-79DC-4957-9D80-3B731AB85BDB}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Debug|x64 = Debug|x64 20 | Debug|x86 = Debug|x86 21 | Release|Any CPU = Release|Any CPU 22 | Release|x64 = Release|x64 23 | Release|x86 = Release|x86 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {61EE2786-BFEA-45D0-8AF7-BD76DE47701B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {61EE2786-BFEA-45D0-8AF7-BD76DE47701B}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {61EE2786-BFEA-45D0-8AF7-BD76DE47701B}.Debug|x64.ActiveCfg = Debug|Any CPU 32 | {61EE2786-BFEA-45D0-8AF7-BD76DE47701B}.Debug|x64.Build.0 = Debug|Any CPU 33 | {61EE2786-BFEA-45D0-8AF7-BD76DE47701B}.Debug|x86.ActiveCfg = Debug|Any CPU 34 | {61EE2786-BFEA-45D0-8AF7-BD76DE47701B}.Debug|x86.Build.0 = Debug|Any CPU 35 | {61EE2786-BFEA-45D0-8AF7-BD76DE47701B}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {61EE2786-BFEA-45D0-8AF7-BD76DE47701B}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {61EE2786-BFEA-45D0-8AF7-BD76DE47701B}.Release|x64.ActiveCfg = Release|Any CPU 38 | {61EE2786-BFEA-45D0-8AF7-BD76DE47701B}.Release|x64.Build.0 = Release|Any CPU 39 | {61EE2786-BFEA-45D0-8AF7-BD76DE47701B}.Release|x86.ActiveCfg = Release|Any CPU 40 | {61EE2786-BFEA-45D0-8AF7-BD76DE47701B}.Release|x86.Build.0 = Release|Any CPU 41 | {975E9B88-80E4-4925-819F-55663ACCC7D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {975E9B88-80E4-4925-819F-55663ACCC7D4}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {975E9B88-80E4-4925-819F-55663ACCC7D4}.Debug|x64.ActiveCfg = Debug|Any CPU 44 | {975E9B88-80E4-4925-819F-55663ACCC7D4}.Debug|x64.Build.0 = Debug|Any CPU 45 | {975E9B88-80E4-4925-819F-55663ACCC7D4}.Debug|x86.ActiveCfg = Debug|Any CPU 46 | {975E9B88-80E4-4925-819F-55663ACCC7D4}.Debug|x86.Build.0 = Debug|Any CPU 47 | {975E9B88-80E4-4925-819F-55663ACCC7D4}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {975E9B88-80E4-4925-819F-55663ACCC7D4}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {975E9B88-80E4-4925-819F-55663ACCC7D4}.Release|x64.ActiveCfg = Release|Any CPU 50 | {975E9B88-80E4-4925-819F-55663ACCC7D4}.Release|x64.Build.0 = Release|Any CPU 51 | {975E9B88-80E4-4925-819F-55663ACCC7D4}.Release|x86.ActiveCfg = Release|Any CPU 52 | {975E9B88-80E4-4925-819F-55663ACCC7D4}.Release|x86.Build.0 = Release|Any CPU 53 | {4146B8F5-27B5-493D-9F74-4AA530469562}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {4146B8F5-27B5-493D-9F74-4AA530469562}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {4146B8F5-27B5-493D-9F74-4AA530469562}.Debug|x64.ActiveCfg = Debug|Any CPU 56 | {4146B8F5-27B5-493D-9F74-4AA530469562}.Debug|x64.Build.0 = Debug|Any CPU 57 | {4146B8F5-27B5-493D-9F74-4AA530469562}.Debug|x86.ActiveCfg = Debug|Any CPU 58 | {4146B8F5-27B5-493D-9F74-4AA530469562}.Debug|x86.Build.0 = Debug|Any CPU 59 | {4146B8F5-27B5-493D-9F74-4AA530469562}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {4146B8F5-27B5-493D-9F74-4AA530469562}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {4146B8F5-27B5-493D-9F74-4AA530469562}.Release|x64.ActiveCfg = Release|Any CPU 62 | {4146B8F5-27B5-493D-9F74-4AA530469562}.Release|x64.Build.0 = Release|Any CPU 63 | {4146B8F5-27B5-493D-9F74-4AA530469562}.Release|x86.ActiveCfg = Release|Any CPU 64 | {4146B8F5-27B5-493D-9F74-4AA530469562}.Release|x86.Build.0 = Release|Any CPU 65 | {918E5168-79DC-4957-9D80-3B731AB85BDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {918E5168-79DC-4957-9D80-3B731AB85BDB}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {918E5168-79DC-4957-9D80-3B731AB85BDB}.Debug|x64.ActiveCfg = Debug|Any CPU 68 | {918E5168-79DC-4957-9D80-3B731AB85BDB}.Debug|x64.Build.0 = Debug|Any CPU 69 | {918E5168-79DC-4957-9D80-3B731AB85BDB}.Debug|x86.ActiveCfg = Debug|Any CPU 70 | {918E5168-79DC-4957-9D80-3B731AB85BDB}.Debug|x86.Build.0 = Debug|Any CPU 71 | {918E5168-79DC-4957-9D80-3B731AB85BDB}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {918E5168-79DC-4957-9D80-3B731AB85BDB}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {918E5168-79DC-4957-9D80-3B731AB85BDB}.Release|x64.ActiveCfg = Release|Any CPU 74 | {918E5168-79DC-4957-9D80-3B731AB85BDB}.Release|x64.Build.0 = Release|Any CPU 75 | {918E5168-79DC-4957-9D80-3B731AB85BDB}.Release|x86.ActiveCfg = Release|Any CPU 76 | {918E5168-79DC-4957-9D80-3B731AB85BDB}.Release|x86.Build.0 = Release|Any CPU 77 | EndGlobalSection 78 | GlobalSection(NestedProjects) = preSolution 79 | {4146B8F5-27B5-493D-9F74-4AA530469562} = {975E9B88-80E4-4925-819F-55663ACCC7D4} 80 | {918E5168-79DC-4957-9D80-3B731AB85BDB} = {3799082F-4B66-419A-82B7-051BAD304718} 81 | EndGlobalSection 82 | EndGlobal 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ply 2 | 3 | [![NuGet Version](https://img.shields.io/nuget/v/Ply.svg)](https://www.nuget.org/packages/Ply) 4 | 5 | ## Ply is a high performance TPL library for F#. 6 | The goal of Ply is to be a very low overhead Task abstraction like it is in C#. 7 | 8 | ### Benchmark 9 | [see benchmark code](https://github.com/crowded/ply/blob/master/Benchmarks/Benchmarks.fs#L33) 10 | 11 | | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | 12 | |------------------------ |---------:|----------:|----------:|------:|--------:|------------:|------------:|------------:| 13 | | C# Async Await | 24.59 us | 0.8028 us | 0.8923 us | 1.00 | 0.00 | 0.2136 | - | - | 14 | | Ply | 24.60 us | 1.1610 us | 1.3371 us | 1.00 | 0.07 | 0.3052 | - | - | 15 | | TaskBuilder.fs v2.1.0 | 26.86 us | 0.6751 us | 0.6932 us | 1.09 | 0.03 | 0.5798 | - | - | 16 | 17 | *Allocated Memory/Op is removed as it isn't correct on .NET Core, Gen 0/1k Op is the relevant metric.* 18 | 19 | ### Builders 20 | Ply comes bundled with these builders: 21 | 22 | | builder | return type | tfm | namespace | 23 | |---------------|---------------|-------------------------------|------------------------------------| 24 | | `task` | Task<'T> | netstandard2.0, netcoreapp2.1 | FSharp.Control.Tasks | 25 | | `vtask` | ValueTask<'T> | netcoreapp2.1 | FSharp.Control.Tasks | 26 | | `unitTask` | Task | netstandard2.0, netcoreapp2.1 | FSharp.Control.Tasks | 27 | | `unitVtask` | ValueTask | netcoreapp2.1 | FSharp.Control.Tasks | 28 | | `uvtask` | ValueTask<'T> | netcoreapp2.1 | FSharp.Control.Tasks.Affine.Unsafe | 29 | | `uunitTask` | Task | netcoreapp2.1 | FSharp.Control.Tasks.Affine.Unsafe | 30 | | `uunintVtask` | ValueTask | netcoreapp2.1 | FSharp.Control.Tasks.Affine.Unsafe | 31 | | `uply` | Ply<'T> | netstandard2.0,netcoreapp2.1 | FSharp.Control.Tasks.Affine.Unsafe | 32 | 33 | 34 | More information on when to use which builder: 35 | 36 | | builder | description | 37 | |--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 38 | | `vtask` | Near zero allocation CE, allocates one object for the [execution bubble](#execution-bubble) at the start, and two objects per bind if the Task we bind to isn't completed yet. | 39 | | `unitTask` | As the TPL doesn't know about F#'s unit type `Task.FromResult(())` won't ever return a cached task. On `netcoreapp` we can check for a succesful completion instead to directly return a CompletedTask removing the task allocation. | 40 | | `unitVtask` | CE shorthand for doing `if vtask.IsCompletedSuccessfully then ValueTask() else ValueTask(vtask.AsTask() :> Task)` | 41 | | `uvtask` | An unsafe version of `vtask` and one of the few zero allocation* CEs Ply comes with. Read about the trade-off under [execution bubble](#execution-bubble) | 42 | | `uunitTask` | An unsafe version of `unitTask` and one of the few zero allocation* CEs Ply comes with. Read about the trade-off under [execution bubble](#execution-bubble) | 43 | | `uunitVtask` | An unsafe version of `unitVtask` and one of the few zero allocation* CEs Ply comes with. Read about the trade-off under [execution bubble](#execution-bubble) | 44 | | `uply` | Can be enqueued directly onto the caller's state machine, skips `Task` and [execution bubble](#execution-bubble). | 45 | 46 | **zero allocation only when any Task (or Task-like) you bind against is already completed.* 47 | 48 | ### Execution bubble 49 | An execution bubble is made by any C# async-await method for capturing and restoring async local and synchronization context changes. Any changes would otherwise escape onto the caller context. 50 | 51 | It's rare that methods do anything with async locals or synchronization contexts, even in C#. 52 | So if you know anything you use doesn't do that either then there's nothing inherently unsafe about using the Unsafe CEs as you don't need any execution bubble for correctness. 53 | 54 | ## Special Thanks 55 | Thanks to @gusty for very valuable SRTP advice, it helped me tremendously to narrow down what specifically was wrong about an earlier approach I took. 56 | 57 | Thanks to @rspeele TaskBuilder.fs was a great inspiration in developing Ply. 58 | 59 | ## Next Steps and Improvements 60 | - Finish up the experimental branch. 61 | - On master we are at 2 allocations per bind, which are the focus of upcoming work. Then there are the few constant factor allocations which are inevitable and equivalent to C# semantics for async methods. 62 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "rollForward": "feature", 4 | "version": "3.1.100" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | PLY_NUGET_KEY=${PLY_NUGET_KEY?env var with nuget credentials is not set} 4 | version="$(xmllint --xpath '//Project/PropertyGroup/Version/text()' Ply.fsproj)" 5 | 6 | dotnet pack Ply.fsproj -c Release -p:ContinuousIntegrationBuild=true \ 7 | && dotnet nuget push "bin/Release/Ply.${version}.nupkg" -s nuget.org --interactive -k $PLY_NUGET_KEY 8 | 9 | -------------------------------------------------------------------------------- /tests/Ply.Tests/Ply.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/Ply.Tests/Program.fs: -------------------------------------------------------------------------------- 1 | module Program = let [] main _ = 0 2 | -------------------------------------------------------------------------------- /tests/Ply.Tests/Tests.fs: -------------------------------------------------------------------------------- 1 | module Tests 2 | 3 | open System 4 | open System.Collections.Generic 5 | open System.Collections 6 | open System.Diagnostics 7 | open System.Runtime.ExceptionServices 8 | open System.Threading 9 | open System.Threading.Tasks 10 | open Xunit 11 | 12 | open FSharp.Control.Tasks.NonAffine 13 | 14 | type TaskLike() = 15 | member _.GetAwaiter() = Task.Yield().GetAwaiter() 16 | 17 | let ``Non-affine taskLike without ConfigureAwait should work`` = 18 | task { 19 | let! x = TaskLike() 20 | return () 21 | } 22 | 23 | type ConfigurableTaskLike() = 24 | member _.ConfigureAwait(b) = Task.Yield() 25 | 26 | let ``Non-affine taskLike with ConfigureAwait should work`` = 27 | task { 28 | let! x = ConfigurableTaskLike() 29 | return () 30 | } 31 | 32 | open FSharp.Control.Tasks 33 | 34 | type AffineTaskLike() = 35 | member _.GetAwaiter() = Task.Yield().GetAwaiter() 36 | 37 | let ``Affine taskLike should work`` = 38 | task { 39 | let! x = AffineTaskLike() 40 | return () 41 | } 42 | 43 | 44 | [] 45 | let ``ediPly should work`` () = 46 | let p = Unsafe.uply { 47 | try 48 | let! r = Task.FromException(InvalidOperationException("Expected")) 49 | return r 50 | with ex -> return ExceptionDispatchInfo.Throw(ex) 51 | } 52 | 53 | Assert.ThrowsAsync(typeof, fun () -> unitTask { 54 | return! p 55 | }) 56 | 57 | 58 | [] 59 | let ``Combine should handle side effects of left portion`` () = 60 | let testTask () = 61 | unitTask { 62 | try 63 | do! Task.Yield () 64 | failwith "Catch me" 65 | with _ -> invalidOp "Fail now" 66 | invalidArg "" "Test failed" 67 | } 68 | Assert.ThrowsAsync(typeof, testTask) 69 | 70 | [] 71 | let ``Try finally should handle side effects of awaitable`` () = 72 | let testTask () = 73 | unitTask { 74 | try 75 | do! Task.Yield() 76 | invalidOp "Fail now" 77 | finally () 78 | } 79 | Assert.ThrowsAsync(typeof, testTask) 80 | 81 | [] 82 | let ``Try finally should handle side effects of finally body`` () = 83 | let testTask () = 84 | task { 85 | let mutable x = false 86 | try do! Task.Yield() 87 | finally x <- true 88 | return x 89 | } 90 | task { 91 | let! b = testTask() 92 | Assert.True(b) 93 | } 94 | 95 | [] 96 | let ``Loop should handle side effects of awaitable on first run`` () = 97 | let testTask () = 98 | unitTask { 99 | let mutable i = 0 100 | while i < 1 do 101 | do! Task.Yield() 102 | if i = 0 then invalidOp "Fail now" 103 | i <- i + 1 104 | } 105 | Assert.ThrowsAsync(typeof, testTask) 106 | 107 | [] 108 | let ``Loop should handle side effects of awaitable on later runs`` () = 109 | let testTask () = 110 | unitTask { 111 | let mutable i = 0 112 | while i < 5 do 113 | do! Task.Yield() 114 | if i = 4 then invalidOp "Fail now" 115 | i <- i + 1 116 | } 117 | Assert.ThrowsAsync(typeof, testTask) 118 | 119 | [] 120 | let ``For loop should not mutate enumerator if loop awaitable completed`` () = 121 | let list = [async{return 1}; async{return 2}; async{return 3}; async{return 4}] 122 | let testTask () = 123 | task { 124 | let mutable state = [] 125 | for x in list do 126 | let! i = x 127 | state <- i::state 128 | return state 129 | } 130 | task { 131 | let! list = testTask() 132 | Assert.Equal(list.Length, 4) 133 | } 134 | 135 | [] 136 | let ``Should accept null disposable`` () = 137 | let testTask () = 138 | task { 139 | use x = null 140 | return true 141 | } 142 | task { 143 | let! b = testTask() 144 | Assert.True(b) 145 | } 146 | 147 | 148 | exception TestException of string 149 | 150 | let require (x: bool) msg = Assert.True(x, msg) 151 | 152 | [] 153 | let testShortCircuitResult() = 154 | let t = 155 | task { 156 | let! x = Task.FromResult(1) 157 | let! y = Task.FromResult(2) 158 | return x + y 159 | } 160 | require t.IsCompleted "didn't short-circuit already completed tasks" 161 | require (t.Result = 3) "wrong result" 162 | 163 | [] 164 | let testDelay() = 165 | let mutable x = 0 166 | let t = 167 | task { 168 | do! Task.Delay(50) 169 | x <- x + 1 170 | } 171 | require (x = 0) "task already ran" 172 | t.Wait() 173 | 174 | [] 175 | let testNoDelay() = 176 | let mutable x = 0 177 | let t = 178 | task { 179 | x <- x + 1 180 | do! Task.Delay(5) 181 | x <- x + 1 182 | } 183 | require (x = 1) "first part didn't run yet" 184 | t.Wait() 185 | 186 | [] 187 | let testNonBlocking() = 188 | let sw = Stopwatch() 189 | sw.Start() 190 | let t = 191 | task { 192 | do! Task.Yield() 193 | Thread.Sleep(100) 194 | } 195 | sw.Stop() 196 | require (sw.ElapsedMilliseconds < 50L) "sleep blocked caller" 197 | t.Wait() 198 | 199 | let failtest str = raise (TestException str) 200 | 201 | [] 202 | let testCatching1() = 203 | let mutable x = 0 204 | let mutable y = 0 205 | let t = 206 | task { 207 | try 208 | do! Task.Delay(0) 209 | failtest "hello" 210 | x <- 1 211 | do! Task.Delay(100) 212 | with 213 | | TestException msg -> 214 | require (msg = "hello") "message tampered" 215 | | _ -> 216 | require false "other exn type" 217 | y <- 1 218 | } 219 | t.Wait() 220 | require (y = 1) "bailed after exn" 221 | require (x = 0) "ran past failure" 222 | 223 | [] 224 | let testCatching2() = 225 | let mutable x = 0 226 | let mutable y = 0 227 | let t = 228 | task { 229 | try 230 | do! Task.Yield() // can't skip through this 231 | failtest "hello" 232 | x <- 1 233 | do! Task.Delay(100) 234 | with 235 | | TestException msg -> 236 | require (msg = "hello") "message tampered" 237 | | _ -> 238 | require false "other exn type" 239 | y <- 1 240 | } 241 | t.Wait() 242 | require (y = 1) "bailed after exn" 243 | require (x = 0) "ran past failure" 244 | 245 | [] 246 | let testNestedCatching() = 247 | let mutable counter = 1 248 | let mutable caughtInner = 0 249 | let mutable caughtOuter = 0 250 | let t1() = 251 | task { 252 | try 253 | do! Task.Yield() 254 | failtest "hello" 255 | with 256 | | TestException msg as exn -> 257 | caughtInner <- counter 258 | counter <- counter + 1 259 | raise exn 260 | } 261 | let t2 = 262 | task { 263 | try 264 | do! t1() 265 | with 266 | | TestException msg as exn -> 267 | caughtOuter <- counter 268 | raise exn 269 | | e -> 270 | require false (sprintf "invalid msg type %s" e.Message) 271 | } 272 | try 273 | t2.Wait() 274 | require false "ran past failed task wait" 275 | with 276 | | :? AggregateException as exn -> 277 | require (exn.InnerExceptions.Count = 1) "more than 1 exn" 278 | require (caughtInner = 1) "didn't catch inner" 279 | require (caughtOuter = 2) "didn't catch outer" 280 | 281 | [] 282 | let testTryFinallyHappyPath() = 283 | let mutable ran = false 284 | let t = 285 | task { 286 | try 287 | require (not ran) "ran way early" 288 | do! Task.Delay(100) 289 | require (not ran) "ran kinda early" 290 | finally 291 | ran <- true 292 | } 293 | t.Wait() 294 | require ran "never ran" 295 | 296 | [] 297 | let testTryFinallySadPath() = 298 | let mutable ran = false 299 | let t = 300 | task { 301 | try 302 | require (not ran) "ran way early" 303 | do! Task.Delay(100) 304 | require (not ran) "ran kinda early" 305 | failtest "uhoh" 306 | finally 307 | ran <- true 308 | } 309 | try 310 | t.Wait() 311 | with 312 | | :? AggregateException as e -> 313 | match e.InnerExceptions |> Seq.toList with 314 | | [TestException "uhoh"] -> () 315 | | _ -> raise e 316 | | e -> raise e 317 | require ran "never ran" 318 | 319 | [] 320 | let testTryFinallyCaught() = 321 | let mutable ran = false 322 | let t = 323 | task { 324 | try 325 | try 326 | require (not ran) "ran way early" 327 | do! Task.Delay(100) 328 | require (not ran) "ran kinda early" 329 | failtest "uhoh" 330 | finally 331 | ran <- true 332 | return 1 333 | with 334 | | TestException "uhoh" -> 335 | return 2 336 | | e -> 337 | raise e 338 | return 3 339 | } 340 | require (t.Result = 2) "wrong return" 341 | require ran "never ran" 342 | 343 | [] 344 | let testUsing() = 345 | let mutable disposed = false 346 | let t = 347 | task { 348 | use d = { new IDisposable with member __.Dispose() = disposed <- true } 349 | require (not disposed) "disposed way early" 350 | do! Task.Delay(100) 351 | require (not disposed) "disposed kinda early" 352 | } 353 | t.Wait() 354 | require disposed "never disposed" 355 | 356 | [] 357 | let testUsingFromTask() = 358 | let mutable disposedInner = false 359 | let mutable disposed = false 360 | let t = 361 | task { 362 | use! d = 363 | task { 364 | do! Task.Delay(50) 365 | use i = { new IDisposable with member __.Dispose() = disposedInner <- true } 366 | require (not disposed && not disposedInner) "disposed inner early" 367 | return { new IDisposable with member __.Dispose() = disposed <- true } 368 | } 369 | require disposedInner "did not dispose inner after task completion" 370 | require (not disposed) "disposed way early" 371 | do! Task.Delay(50) 372 | require (not disposed) "disposed kinda early" 373 | } 374 | t.Wait() 375 | require disposed "never disposed" 376 | 377 | [] 378 | let testUsingSadPath() = 379 | let mutable disposedInner = false 380 | let mutable disposed = false 381 | let t = 382 | task { 383 | try 384 | use! d = 385 | task { 386 | do! Task.Delay(50) 387 | use i = { new IDisposable with member __.Dispose() = disposedInner <- true } 388 | failtest "uhoh" 389 | require (not disposed && not disposedInner) "disposed inner early" 390 | return { new IDisposable with member __.Dispose() = disposed <- true } 391 | } 392 | () 393 | with 394 | | TestException msg -> 395 | require disposedInner "did not dispose inner after task completion" 396 | require (not disposed) "disposed way early" 397 | do! Task.Delay(50) 398 | require (not disposed) "disposed kinda early" 399 | } 400 | t.Wait() 401 | require (not disposed) "disposed thing that never should've existed" 402 | 403 | [] 404 | let testForLoop() = 405 | let mutable disposed = false 406 | let wrapList = 407 | let raw = ["a"; "b"; "c"] |> Seq.ofList 408 | let getEnumerator() = 409 | let raw = raw.GetEnumerator() 410 | { new IEnumerator with 411 | member __.MoveNext() = 412 | require (not disposed) "moved next after disposal" 413 | raw.MoveNext() 414 | member __.Current = 415 | require (not disposed) "accessed current after disposal" 416 | raw.Current 417 | member __.Current = 418 | require (not disposed) "accessed current (boxed) after disposal" 419 | box raw.Current 420 | member __.Dispose() = 421 | require (not disposed) "disposed twice" 422 | disposed <- true 423 | raw.Dispose() 424 | member __.Reset() = 425 | require (not disposed) "reset after disposal" 426 | raw.Reset() 427 | } 428 | { new IEnumerable with 429 | member __.GetEnumerator() : IEnumerator = getEnumerator() 430 | member __.GetEnumerator() : IEnumerator = upcast getEnumerator() 431 | } 432 | let t = 433 | task { 434 | let mutable index = 0 435 | do! Task.Yield() 436 | for x in wrapList do 437 | do! Task.Yield() 438 | match index with 439 | | 0 -> require (x = "a") "wrong first value" 440 | | 1 -> require (x = "b") "wrong second value" 441 | | 2 -> require (x = "c") "wrong third value" 442 | | _ -> require false "iterated too far!" 443 | index <- index + 1 444 | do! Task.Yield() 445 | do! Task.Yield() 446 | return 1 447 | } 448 | t.Wait() 449 | require disposed "never disposed" 450 | 451 | [] 452 | let testForLoopSadPath() = 453 | let mutable disposed = false 454 | let wrapList = 455 | let raw = ["a"; "b"; "c"] |> Seq.ofList 456 | let getEnumerator() = 457 | let raw = raw.GetEnumerator() 458 | { new IEnumerator with 459 | member __.MoveNext() = 460 | require (not disposed) "moved next after disposal" 461 | raw.MoveNext() 462 | member __.Current = 463 | require (not disposed) "accessed current after disposal" 464 | raw.Current 465 | member __.Current = 466 | require (not disposed) "accessed current (boxed) after disposal" 467 | box raw.Current 468 | member __.Dispose() = 469 | require (not disposed) "disposed twice" 470 | disposed <- true 471 | raw.Dispose() 472 | member __.Reset() = 473 | require (not disposed) "reset after disposal" 474 | raw.Reset() 475 | } 476 | { new IEnumerable with 477 | member __.GetEnumerator() : IEnumerator = getEnumerator() 478 | member __.GetEnumerator() : IEnumerator = upcast getEnumerator() 479 | } 480 | let mutable caught = false 481 | let t = 482 | task { 483 | try 484 | let mutable index = 0 485 | do! Task.Yield() 486 | for x in wrapList do 487 | do! Task.Yield() 488 | match index with 489 | | 0 -> require (x = "a") "wrong first value" 490 | | _ -> failtest "uhoh" 491 | index <- index + 1 492 | do! Task.Yield() 493 | do! Task.Yield() 494 | return 1 495 | with 496 | | TestException "uhoh" -> 497 | caught <- true 498 | return 2 499 | } 500 | require (t.Result = 2) "wrong result" 501 | require caught "didn't catch exception" 502 | require disposed "never disposed" 503 | 504 | [] 505 | let testExceptionAttachedToTaskWithoutAwait() = 506 | let mutable ranA = false 507 | let mutable ranB = false 508 | let t = 509 | task { 510 | ranA <- true 511 | failtest "uhoh" 512 | ranB <- true 513 | } 514 | require ranA "didn't run immediately" 515 | require (not ranB) "ran past exception" 516 | require (not (isNull t.Exception)) "didn't capture exception" 517 | require (t.Exception.InnerExceptions.Count = 1) "captured more exceptions" 518 | require (t.Exception.InnerException = TestException "uhoh") "wrong exception" 519 | let mutable caught = false 520 | let mutable ranCatcher = false 521 | let catcher = 522 | task { 523 | try 524 | ranCatcher <- true 525 | let! result = t 526 | return false 527 | with 528 | | TestException "uhoh" -> 529 | caught <- true 530 | return true 531 | } 532 | require ranCatcher "didn't run" 533 | require catcher.Result "didn't catch" 534 | require caught "didn't catch" 535 | 536 | [] 537 | let testExceptionAttachedToTaskWithAwait() = 538 | let mutable ranA = false 539 | let mutable ranB = false 540 | let t = 541 | task { 542 | ranA <- true 543 | failtest "uhoh" 544 | do! Task.Delay(100) 545 | ranB <- true 546 | } 547 | require ranA "didn't run immediately" 548 | require (not ranB) "ran past exception" 549 | require (not (isNull t.Exception)) "didn't capture exception" 550 | require (t.Exception.InnerExceptions.Count = 1) "captured more exceptions" 551 | require (t.Exception.InnerException = TestException "uhoh") "wrong exception" 552 | let mutable caught = false 553 | let mutable ranCatcher = false 554 | let catcher = 555 | task { 556 | try 557 | ranCatcher <- true 558 | let! result = t 559 | return false 560 | with 561 | | TestException "uhoh" -> 562 | caught <- true 563 | return true 564 | } 565 | require ranCatcher "didn't run" 566 | require catcher.Result "didn't catch" 567 | require caught "didn't catch" 568 | 569 | [] 570 | let testExceptionThrownInFinally() = 571 | let mutable ranInitial = false 572 | let mutable ranNext = false 573 | let mutable ranFinally = 0 574 | let t = 575 | task { 576 | try 577 | ranInitial <- true 578 | do! Task.Yield() 579 | Thread.Sleep(100) // shouldn't be blocking so we should get through to requires before this finishes 580 | ranNext <- true 581 | finally 582 | ranFinally <- ranFinally + 1 583 | failtest "finally exn!" 584 | } 585 | require ranInitial "didn't run initial" 586 | require (not ranNext) "ran next too early" 587 | try 588 | t.Wait() 589 | require false "shouldn't get here" 590 | with 591 | | _ -> () 592 | require ranNext "didn't run next" 593 | require (ranFinally = 1) "didn't run finally exactly once" 594 | 595 | [] 596 | let test2ndExceptionThrownInFinally() = 597 | let mutable ranInitial = false 598 | let mutable ranNext = false 599 | let mutable ranFinally = 0 600 | let t = 601 | task { 602 | try 603 | ranInitial <- true 604 | do! Task.Yield() 605 | Thread.Sleep(100) // shouldn't be blocking so we should get through to requires before this finishes 606 | ranNext <- true 607 | failtest "uhoh" 608 | finally 609 | ranFinally <- ranFinally + 1 610 | failtest "2nd exn!" 611 | } 612 | require ranInitial "didn't run initial" 613 | require (not ranNext) "ran next too early" 614 | try 615 | t.Wait() 616 | require false "shouldn't get here" 617 | with 618 | | _ -> () 619 | require ranNext "didn't run next" 620 | require (ranFinally = 1) "didn't run finally exactly once" 621 | 622 | [] 623 | let testFixedStackWhileLoop() = 624 | let bigNumber = 10000 625 | let t = 626 | task { 627 | let mutable maxDepth = Nullable() 628 | let mutable i = 0 629 | while i < bigNumber do 630 | i <- i + 1 631 | do! Task.Yield() 632 | if i % 100 = 0 then 633 | let stackDepth = StackTrace().FrameCount 634 | if maxDepth.HasValue && stackDepth > maxDepth.Value then 635 | failwith "Stack depth increased!" 636 | maxDepth <- Nullable(stackDepth) 637 | return i 638 | } 639 | t.Wait() 640 | require (t.Result = bigNumber) "didn't get to big number" 641 | 642 | [] 643 | let testFixedStackForLoop() = 644 | let bigNumber = 10000 645 | let mutable ran = false 646 | let t = 647 | task { 648 | let mutable maxDepth = Nullable() 649 | for i in Seq.init bigNumber id do 650 | do! Task.Yield() 651 | if i % 100 = 0 then 652 | let stackDepth = StackTrace().FrameCount 653 | if maxDepth.HasValue && stackDepth > maxDepth.Value then 654 | failwith "Stack depth increased!" 655 | maxDepth <- Nullable(stackDepth) 656 | ran <- true 657 | return () 658 | } 659 | t.Wait() 660 | require ran "didn't run all" 661 | 662 | [] 663 | let testTypeInference() = 664 | let t1 : string Task = 665 | task { 666 | return "hello" 667 | } 668 | let t2 = 669 | task { 670 | let! s = t1 671 | return s.Length 672 | } 673 | t2.Wait() 674 | 675 | [] 676 | let testNoStackOverflowWithImmediateResult() = 677 | let longLoop = 678 | task { 679 | let mutable n = 0 680 | while n < 10_000 do 681 | n <- n + 1 682 | return! Task.FromResult(()) 683 | } 684 | longLoop.Wait() 685 | 686 | [] 687 | let testNoStackOverflowWithYieldResult() = 688 | let longLoop = 689 | task { 690 | let mutable n = 0 691 | while n < 10_000 do 692 | let! _ = 693 | task { 694 | do! Task.Yield() 695 | let! _ = Task.FromResult(0) 696 | n <- n + 1 697 | } 698 | n <- n + 1 699 | } 700 | longLoop.Wait() 701 | 702 | [] 703 | let testSmallTailRecursion() = 704 | let shortLoop = 705 | task { 706 | let rec loop n = 707 | task { 708 | // larger N would stack overflow on Mono, eat heap mem on MS .NET 709 | if n < 1000 then 710 | do! Task.Yield() 711 | let! _ = Task.FromResult(0) 712 | return! loop (n + 1) 713 | else 714 | return () 715 | } 716 | return! loop 0 717 | } 718 | shortLoop.Wait() 719 | 720 | [] 721 | let testTryOverReturnFrom() = 722 | let inner() = 723 | task { 724 | do! Task.Yield() 725 | failtest "inner" 726 | return 1 727 | } 728 | let t = 729 | task { 730 | try 731 | do! Task.Yield() 732 | return! inner() 733 | with 734 | | TestException "inner" -> return 2 735 | } 736 | require (t.Result = 2) "didn't catch" 737 | 738 | [] 739 | let testTryFinallyOverReturnFromWithException() = 740 | let inner() = 741 | task { 742 | do! Task.Yield() 743 | failtest "inner" 744 | return 1 745 | } 746 | let mutable m = 0 747 | let t = 748 | task { 749 | try 750 | do! Task.Yield() 751 | return! inner() 752 | finally 753 | m <- 1 754 | } 755 | try 756 | t.Wait() 757 | with 758 | | :? AggregateException -> () 759 | require (m = 1) "didn't run finally" 760 | 761 | [] 762 | let testTryFinallyOverReturnFromWithoutException() = 763 | let inner() = 764 | task { 765 | do! Task.Yield() 766 | return 1 767 | } 768 | let mutable m = 0 769 | let t = 770 | task { 771 | try 772 | do! Task.Yield() 773 | return! inner() 774 | finally 775 | m <- 1 776 | } 777 | try 778 | t.Wait() 779 | with 780 | | :? AggregateException -> () 781 | require (m = 1) "didn't run finally" 782 | 783 | // no need to call this, we just want to check that it compiles w/o warnings 784 | let testTrivialReturnCompiles (x : 'a) : 'a Task = 785 | task { 786 | do! Task.Yield() 787 | return x 788 | } 789 | 790 | // no need to call this, we just want to check that it compiles w/o warnings 791 | let testTrivialTransformedReturnCompiles (x : 'a) (f : 'a -> 'b) : 'b Task = 792 | task { 793 | do! Task.Yield() 794 | return f x 795 | } 796 | 797 | type ITaskThing = 798 | abstract member Taskify : 'a option -> 'a Task 799 | 800 | // no need to call this, we just want to check that it compiles w/o warnings 801 | let testInterfaceUsageCompiles (iface : ITaskThing) (x : 'a) : 'a Task = 802 | task { 803 | let! xResult = iface.Taskify (Some x) 804 | do! Task.Yield() 805 | return xResult 806 | } 807 | 808 | [] 809 | let testAsyncsMixedWithTasks() = 810 | let t = 811 | task { 812 | do! Task.Delay(1) 813 | do! Async.Sleep(1) 814 | let! x = 815 | async { 816 | do! Async.Sleep(1) 817 | return 5 818 | } 819 | return! async { return x + 3 } 820 | } 821 | let result = t.Result 822 | require (result = 8) "something weird happened" 823 | 824 | // no need to call this, we just want to check that it compiles w/o warnings 825 | let testDefaultInferenceForReturnFrom() = 826 | let t = task { return Some "x" } 827 | task { 828 | let! r = t 829 | if r = None then 830 | return! (failwith "Could not find x") 831 | else 832 | return r 833 | } 834 | 835 | // no need to call this, just check that it compiles 836 | let testCompilerInfersArgumentOfReturnFrom = 837 | task { 838 | if true then return 1 839 | else return! failwith "" 840 | } 841 | 842 | // no need to call this, just check that it compiles 843 | let testCompilerInfersArgumentOfLetBang() = 844 | task { 845 | let! x = Unchecked.defaultof<_> 846 | return x 847 | } 848 | -------------------------------------------------------------------------------- /tests/multi-targetting/netcoreapp2.1-directdep/Program.fs: -------------------------------------------------------------------------------- 1 | // Learn more about F# at http://fsharp.org 2 | 3 | open FSharp.Control.Tasks.Builders 4 | open System.Threading.Tasks 5 | 6 | module Say = 7 | let hello name = task { 8 | let! x = Task.FromResult(name) 9 | printfn "Hello %s" x 10 | } 11 | 12 | let helloVtask name = vtask { 13 | let! x = Task.FromResult(name) 14 | printfn "Hello %s" x 15 | } 16 | 17 | 18 | open Say 19 | 20 | [] 21 | let main argv = 22 | (hello "ply").GetAwaiter().GetResult() 23 | (helloVtask "ply").GetAwaiter().GetResult() 24 | 0 // return an integer exit code 25 | -------------------------------------------------------------------------------- /tests/multi-targetting/netcoreapp2.1-directdep/netcoreapp2.1.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | netcoreapp21 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/multi-targetting/netcoreapp2.1-ns2.0dep/Program.fs: -------------------------------------------------------------------------------- 1 | // Learn more about F# at http://fsharp.org 2 | 3 | open netstandard2.Say 4 | 5 | [] 6 | let main argv = 7 | 8 | (hello "ply").GetAwaiter().GetResult() 9 | (helloVtask "ply").GetAwaiter().GetResult() 10 | 0 // return an integer exit code 11 | -------------------------------------------------------------------------------- /tests/multi-targetting/netcoreapp2.1-ns2.0dep/netcoreapp2.1.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | netcoreapp21 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/multi-targetting/netcoreapp3.1-ns2.1dep/Program.fs: -------------------------------------------------------------------------------- 1 | // Learn more about F# at http://fsharp.org 2 | 3 | open netstandard21.Say 4 | 5 | [] 6 | let main argv = 7 | 8 | (hello "ply").GetAwaiter().GetResult() 9 | (helloVtask "ply").GetAwaiter().GetResult() 10 | 0 // return an integer exit code 11 | -------------------------------------------------------------------------------- /tests/multi-targetting/netcoreapp3.1-ns2.1dep/netcoreapp3.1.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | netcoreapp31 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/multi-targetting/netstandard2.0/Library.fs: -------------------------------------------------------------------------------- 1 | namespace netstandard2 2 | 3 | open FSharp.Control.Tasks.Builders 4 | open System.Threading.Tasks 5 | 6 | module Say = 7 | let hello name = task { 8 | let! x = Task.FromResult(name) 9 | printfn "Hello %s" x 10 | } 11 | 12 | let helloVtask name = vtask { 13 | let! x = Task.FromResult(name) 14 | printfn "Hello %s" x 15 | } -------------------------------------------------------------------------------- /tests/multi-targetting/netstandard2.0/netstandard2.0.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | netstandard2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/multi-targetting/netstandard2.1/Library.fs: -------------------------------------------------------------------------------- 1 | namespace netstandard21 2 | 3 | open FSharp.Control.Tasks.Builders 4 | open System.Threading.Tasks 5 | 6 | module Say = 7 | let hello name = task { 8 | let! x = Task.FromResult(name) 9 | printfn "Hello %s" x 10 | } 11 | 12 | let helloVtask name = vtask { 13 | let! x = Task.FromResult(name) 14 | printfn "Hello %s" x 15 | } 16 | 17 | -------------------------------------------------------------------------------- /tests/multi-targetting/netstandard2.1/netstandard2.1.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | netstandard21 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/multi-targetting/run.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | pushd netcoreapp2.1-directdep && dotnet run && popd 4 | pushd netcoreapp2.1-ns2.0dep && dotnet run && popd 5 | pushd netcoreapp3.1-ns2.1dep && dotnet run && popd -------------------------------------------------------------------------------- /tools/icons/ply-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crowded/ply/321965a7412714c87b71d8706351eb1b778dac81/tools/icons/ply-128x128.png -------------------------------------------------------------------------------- /tools/icons/ply.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------