diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index cd4b352ec..c7f76f3d9 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -3871,6 +3871,10 @@ let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr lis Helper.LibCall(com, "Async", "catchAsync", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some | "RunSynchronously" -> None + // Task is Promise in JS/TS + | "AwaitTask" -> + Helper.LibCall(com, "Async", "awaitPromise", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + |> Some // Fable.Core extensions | meth -> Helper.LibCall( @@ -3885,6 +3889,71 @@ let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr lis ) |> Some +let taskBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + match thisArg, i.CompiledName, args with + | _, "Singleton", _ -> makeImportLib com t "singleton" "TaskBuilder" |> Some + | Some x, "Using", [ arg; f ] + | Some x, "TaskBuilderBase.Using", [ arg; f ] -> + Helper.InstanceCall(x, "Using", t, [ arg; f ], i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + |> Some + | Some x, "TaskBuilderBase.Bind", [ arg; f ] -> + Helper.InstanceCall(x, "Bind", t, [ arg; f ], i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + |> Some + | Some x, "TaskBuilderBase.ReturnFrom", [ arg ] -> + Helper.InstanceCall(x, "ReturnFrom", t, [ arg ], i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + |> Some + | Some x, meth, _ -> + Helper.InstanceCall(x, meth, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + |> Some + | None, meth, _ -> + Helper.LibCall( + com, + "TaskBuilder", + Naming.lowerFirst meth, + t, + args, + i.SignatureArgTypes, + genArgs = i.GenericArgs, + ?loc = r + ) + |> Some + +let tasks (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + match thisArg, i.CompiledName with + | Some x, ("GetAwaiter" | "GetResult" | "get_Result" | "Result") -> + // Task = Promise; return the promise - callers use Async.AwaitTask to extract the value + Some x + | None, "FromResult" -> + Helper.LibCall(com, "Task", "fromResult", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | None, ".ctor" -> + Helper.LibCall( + com, + "Task", + "TaskCompletionSource", + t, + args, + i.SignatureArgTypes, + isConstructor = true, + ?loc = r + ) + |> Some + | Some x, meth -> + Helper.InstanceCall(x, meth, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + |> Some + | None, meth -> + Helper.LibCall( + com, + "Task", + Naming.lowerFirst meth, + t, + args, + i.SignatureArgTypes, + genArgs = i.GenericArgs, + ?loc = r + ) + |> Some + let guids (com: ICompiler) (ctx: Context) @@ -4371,6 +4440,15 @@ let private replacedModules = "Microsoft.FSharp.Control.AsyncActivation`1", asyncBuilder "Microsoft.FSharp.Control.FSharpAsync", asyncs "Microsoft.FSharp.Control.AsyncPrimitives", asyncs + Types.task, tasks + Types.taskGeneric, tasks + "System.Threading.Tasks.TaskCompletionSource`1", tasks + "System.Runtime.CompilerServices.TaskAwaiter`1", tasks + "Microsoft.FSharp.Control.TaskBuilder", taskBuilder + "Microsoft.FSharp.Control.TaskBuilderBase", taskBuilder + "Microsoft.FSharp.Control.TaskBuilderModule", taskBuilder + "Microsoft.FSharp.Control.TaskBuilderExtensions.HighPriority", taskBuilder + "Microsoft.FSharp.Control.TaskBuilderExtensions.LowPriority", taskBuilder Types.guid, guids "System.Uri", uris "System.Lazy`1", laziness diff --git a/src/fable-library-py/fable_library/task_builder.py b/src/fable-library-py/fable_library/task_builder.py index 41921d3d5..976a9aaa4 100644 --- a/src/fable-library-py/fable_library/task_builder.py +++ b/src/fable-library-py/fable_library/task_builder.py @@ -87,17 +87,9 @@ async def try_with() -> T: async def Using[T: IDisposable, U](self, resource: T, binder: Callable[[T], Awaitable[U]]) -> U: return await self.TryFinally(self.Delay(lambda: binder(resource)), lambda: resource.Dispose()) - @overload - async def While(self, guard: Callable[[], bool], computation: Delayed[None]) -> None: ... - - @overload - async def While[T](self, guard: Callable[[], bool], computation: Delayed[T]) -> T: ... - - async def While(self, guard: Callable[[], bool], computation: Delayed[Any]) -> Any: - if guard(): - return await self.Bind(computation(), lambda _: self.While(guard, computation)) - else: - return await self.Return() + async def While(self, guard: Callable[[], bool], computation: Delayed[None]) -> None: + while guard(): + await computation() async def Zero(self) -> None: return await zero() diff --git a/src/fable-library-ts/Task.ts b/src/fable-library-ts/Task.ts new file mode 100644 index 000000000..0bceaabae --- /dev/null +++ b/src/fable-library-ts/Task.ts @@ -0,0 +1,37 @@ +import { OperationCanceledException } from "./AsyncBuilder.ts"; + +export class TaskCompletionSource { + private _resolve!: (value: T) => void; + private _reject!: (reason?: unknown) => void; + public task: Promise; + + constructor() { + this.task = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + } + + SetResult(value: T): void { this._resolve(value); } + SetException(error: unknown): void { this._reject(error); } + SetCancelled(): void { this._reject(new OperationCanceledException()); } + get_Task(): Promise { return this.task; } +} + +export function fromResult(value: T): Promise { + return Promise.resolve(value); +} + +export function zero(): Promise { + return Promise.resolve(); +} + +// Task = Promise in JS/TS. GetAwaiter/GetResult return the Promise itself; +// callers should use Async.AwaitTask to extract the value in an async context. +export function getAwaiter(t: Promise): Promise { + return t; +} + +export function getResult(t: Promise): Promise { + return t; +} diff --git a/src/fable-library-ts/TaskBuilder.ts b/src/fable-library-ts/TaskBuilder.ts new file mode 100644 index 000000000..c4332f096 --- /dev/null +++ b/src/fable-library-ts/TaskBuilder.ts @@ -0,0 +1,85 @@ +import { IDisposable } from "./Util.ts"; +import { zero } from "./Task.ts"; + +export class TaskBuilder { + public Bind(computation: Promise, binder: (x: T) => Promise): Promise { + return computation.then(binder); + } + + public Combine(computation1: Promise, computation2: () => Promise): Promise { + return computation1.then(computation2); + } + + public Delay(generator: () => Promise): () => Promise { + return generator; + } + + public For(sequence: Iterable, body: (x: T) => Promise): Promise { + const iter = sequence[Symbol.iterator](); + let cur = iter.next(); + return this.While( + () => !cur.done, + this.Delay(() => { + const res = body(cur.value!); + cur = iter.next(); + return res; + }) + ); + } + + public Return(value?: T): Promise { + return Promise.resolve(value); + } + + public ReturnFrom(computation: Promise): Promise { + return computation; + } + + public TryFinally(computation: () => Promise, compensation: () => void): Promise { + try { + return computation().finally(compensation); + } catch (e) { + compensation(); + return Promise.reject(e); + } + } + + public TryWith(computation: () => Promise, catchHandler: (e: unknown) => Promise): Promise { + try { + return computation().catch(catchHandler); + } catch (e) { + try { + return catchHandler(e); + } catch (e2) { + return Promise.reject(e2); + } + } + } + + public Using(resource: T, binder: (x: T) => Promise): Promise { + return this.TryFinally(() => binder(resource), () => resource.Dispose()); + } + + public While(guard: () => boolean, computation: () => Promise): Promise { + return (async () => { + while (guard()) { + await computation(); + } + })(); + } + + public Zero(): Promise { + return zero(); + } + + public Run(computation: () => Promise): Promise { + try { + return computation(); + } catch (e) { + return Promise.reject(e); + } + } +} + +export const singleton = new TaskBuilder(); +export function task(): TaskBuilder { return singleton; } diff --git a/tests/Js/Main/Fable.Tests.fsproj b/tests/Js/Main/Fable.Tests.fsproj index 62cdb8584..0602b9333 100644 --- a/tests/Js/Main/Fable.Tests.fsproj +++ b/tests/Js/Main/Fable.Tests.fsproj @@ -42,6 +42,7 @@ + diff --git a/tests/Js/Main/Main.fs b/tests/Js/Main/Main.fs index 6f7fe4ed4..14432b551 100644 --- a/tests/Js/Main/Main.fs +++ b/tests/Js/Main/Main.fs @@ -9,6 +9,7 @@ let allTests = RandomTests.tests Arrays.tests Async.tests + Task.tests Chars.tests Comparison.tests ConditionalWeakTable.tests diff --git a/tests/Js/Main/TaskTests.fs b/tests/Js/Main/TaskTests.fs new file mode 100644 index 000000000..392614cb0 --- /dev/null +++ b/tests/Js/Main/TaskTests.fs @@ -0,0 +1,177 @@ +module Fable.Tests.Task + +open System +open System.Threading.Tasks +open Util.Testing + +type DisposableAction(f) = + interface IDisposable with + member _.Dispose() = f () + +// Bridge pattern: works on .NET (Async.AwaitTask wraps Task) and JS (awaitPromise wraps Promise) +let awaitTask (t: Task<'T>) = Async.AwaitTask t + +let tests = + testList "Task" [ + testCaseAsync "Simple task translates without exception" <| fun () -> + async { + let! _ = awaitTask (task { return () }) + () + } + + testCaseAsync "Simple task result works" <| fun () -> + async { + let! result = awaitTask (task { return 42 }) + equal 42 result + } + + testCaseAsync "Task.FromResult works" <| fun () -> + async { + let! result = awaitTask (Task.FromResult 42) + equal 42 result + } + + testCaseAsync "task while binding works correctly" <| fun () -> + async { + let mutable result = 0 + let! _ = awaitTask (task { + while result < 10 do + result <- result + 1 + }) + equal 10 result + } + + testCaseAsync "task while loop handles large number of iterations without stack overflow" <| fun () -> + async { + let mutable result = 0 + let! _ = awaitTask (task { + while result < 10000 do + result <- result + 1 + }) + equal 10000 result + } + + testCaseAsync "Task for binding works correctly" <| fun () -> + async { + let inputs = [| 1; 2; 3 |] + let mutable result = 0 + let! _ = awaitTask (task { + for inp in inputs do + result <- result + inp + }) + equal 6 result + } + + testCaseAsync "Task exceptions are handled correctly" <| fun () -> + async { + let mutable result = 0 + let run shouldThrow = task { + try + if shouldThrow then failwith "boom!" + else result <- 12 + with _ -> result <- 10 + } + let! _ = awaitTask (run true) + let r1 = result + let! _ = awaitTask (run false) + let r2 = result + equal 22 (r1 + r2) + } + + testCaseAsync "Simple task is executed correctly" <| fun () -> + async { + let mutable result = false + let x = task { return 99 } + let! r = awaitTask (task { + let! x = x + let y = 99 + result <- x = y + }) + equal true result + } + + testCaseAsync "task use statements should dispose of resources when they go out of scope" <| fun () -> + async { + let mutable isDisposed = false + let mutable step1ok = false + let mutable step2ok = false + let resource = task { return new DisposableAction(fun () -> isDisposed <- true) } + let! _ = awaitTask (task { + use! r = resource + step1ok <- not isDisposed + step2ok <- not isDisposed + }) + equal true step1ok + equal true step2ok + equal true isDisposed + } + + testCaseAsync "task let bang works" <| fun () -> + async { + let! result = awaitTask (task { + let! x = task { return 10 } + let! y = task { return 20 } + return x + y + }) + equal 30 result + } + + testCaseAsync "task do bang works" <| fun () -> + async { + let mutable x = 0 + let! result = awaitTask (task { + do! task { x <- 42 } + return x + }) + equal 42 result + } + + testCaseAsync "task return from works" <| fun () -> + async { + let inner = task { return "hello" } + let! result = awaitTask (task { return! inner }) + equal "hello" result + } + + testCaseAsync "task try-with works" <| fun () -> + async { + let mutable result = 0 + let! _ = awaitTask (task { + try failwith "boom" + with _e -> result <- 42 + }) + equal 42 result + } + + testCaseAsync "task sequential composition works" <| fun () -> + async { + let mutable count = 0 + let! result = awaitTask (task { + let! a = task { count <- count + 1; return count } + let! b = task { count <- count + 1; return count } + return a + b + }) + equal 3 result + } + + testCaseAsync "Task exceptions propagate through bind" <| fun () -> + async { + let mutable result = "" + let! _ = awaitTask (task { + try + let! _ = task { failwith "inner"; return 0 } + result <- "no exception" + with ex -> + result <- ex.Message + }) + equal "inner" result + } + + testCaseAsync "TaskCompletionSource works" <| fun () -> + async { + let tcs = TaskCompletionSource() + tcs.SetResult(42) + let! result = awaitTask tcs.Task + equal 42 result + } + ] diff --git a/tests/Python/TestTask.fs b/tests/Python/TestTask.fs index 24fe18e19..e505f3b79 100644 --- a/tests/Python/TestTask.fs +++ b/tests/Python/TestTask.fs @@ -37,6 +37,19 @@ let ``test task while binding works correctly`` () = tsk.GetAwaiter().GetResult() equal result 10 +[] +let ``test task while loop handles large number of iterations without stack overflow`` () = + let mutable result = 0 + + let tsk = + task { + while result < 10000 do + result <- result + 1 + } + + tsk.GetAwaiter().GetResult() + equal result 10000 + [] let ``test Task for binding works correctly`` () = let inputs = [| 1; 2; 3 |]