diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 180e9855e1..4820be930b 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -1718,7 +1718,12 @@ let transformSet (com: IPythonCompiler) ctx range fableExpr typ (value: Fable.Ex let transformBindingExprBody (com: IPythonCompiler) (ctx: Context) (var: Fable.Ident) (value: Fable.Expr) = match value with | Function(args, body) -> - let name = Some var.Name + // decision: mutable callbacks keep their Callable annotation instead of inheriting a def's parameter names + let name = + if var.IsMutable then + None + else + Some var.Name let args', body', returnType, typeParams = Annotation.transformFunctionWithAnnotations com ctx name args body diff --git a/src/fable-library-beam/fable_async.erl b/src/fable-library-beam/fable_async.erl index 96ca37d44b..848b9f6594 100644 --- a/src/fable-library-beam/fable_async.erl +++ b/src/fable-library-beam/fable_async.erl @@ -9,6 +9,7 @@ parallel/1, sequential/1, catch_async/1, + try_cancelled/2, ignore/1, from_continuations/1, start_as_task/1, @@ -43,6 +44,7 @@ -spec parallel(list() | reference()) -> async(reference()). -spec sequential(list()) -> async(list()). -spec catch_async(async(term())) -> async(term()). +-spec try_cancelled(async(term()), fun((term()) -> term())) -> async(term()). -spec ignore(async(term())) -> async(ok). -spec from_continuations(fun()) -> async(term()). -spec start_as_task(async(term())) -> term(). @@ -479,6 +481,21 @@ catch_async(Computation) -> end end. +%% TryCancelled: observe cancellation without changing its terminal outcome. +try_cancelled(Computation, Compensation) -> + fun(Ctx) -> + OnCancel = maps:get(on_cancel, Ctx), + Ctx2 = Ctx#{ + on_cancel => fun(Error) -> + %% decision: preserve the original cancellation even when compensation throws, matching .NET. + try Compensation(Error) catch _:_ -> ok end, + OnCancel(Error) + end + }, + %% invariant: install the handler without adding a cancellation checkpoint, including for nested handlers. + Computation(Ctx2) + end. + %% Ignore: discard result ignore(Computation) -> fable_async_builder:bind(Computation, fun(_) -> fable_async_builder:return(ok) end). diff --git a/src/fable-library-py/fable_library/async_.py b/src/fable-library-py/fable_library/async_.py index 7f08e65750..462b951e8e 100644 --- a/src/fable-library-py/fable_library/async_.py +++ b/src/fable-library-py/fable_library/async_.py @@ -215,6 +215,23 @@ def _arrow21(__unit: Unit = UNIT) -> Async[Array[T]]: return delay(delayed) +def try_cancelled[T](computation: Async[T], compensation: Callable[[OperationCanceledError], None]) -> Async[T]: + def cont(ctx: IAsyncContext[T]) -> None: + def on_cancel(error: OperationCanceledError) -> None: + # decision: preserves the original cancellation when compensation throws, matching .NET + try: + compensation(error) + except Exception: + pass + ctx.trampoline.run(lambda: ctx.on_cancel(error)) + + ctx_ = IAsyncContext.create(ctx.trampoline, ctx.cancel_token, ctx.on_success, ctx.on_error, on_cancel) + # invariant: installs the handler without adding a cancellation checkpoint, including for nested handlers + computation(ctx_) + + return cont + + def catch_async[T](work: Async[T]) -> Async[FSharpChoice_2[T, Exception]]: def cont(ctx: IAsyncContext[FSharpChoice_2[T, Exception]]) -> None: def on_success(x: T): @@ -528,4 +545,5 @@ def exception_handler(loop: asyncio.AbstractEventLoop, context: dict[str, Any]) "start_child", "start_immediate", "start_with_continuations", + "try_cancelled", ] diff --git a/src/fable-library-ts/Async.ts b/src/fable-library-ts/Async.ts index 09eaabee44..75ea833b19 100644 --- a/src/fable-library-ts/Async.ts +++ b/src/fable-library-ts/Async.ts @@ -127,6 +127,24 @@ export function catchAsync(work: Async) { }); } +export function tryCancelled(computation: Async, compensation: (error: OperationCanceledException) => void): Async { + return (ctx) => { + // invariant: install the handler without adding a cancellation checkpoint, including for nested handlers + computation({ + ...ctx, + onCancel: (error) => { + // decision: preserve the original cancellation even when compensation throws, matching .NET + try { compensation(error); } catch { /* Cancellation takes precedence. */ } + if (ctx.trampoline.incrementAndCheck()) { + ctx.trampoline.hijack(() => ctx.onCancel(error)); + } else { + ctx.onCancel(error); + } + }, + }); + }; +} + export function fromContinuations(f: (conts: Continuations) => void) { return (ctx: IAsyncContext) => { let settled = false; diff --git a/tests/Beam/AsyncTests.fs b/tests/Beam/AsyncTests.fs index b318fb914e..364f65d769 100644 --- a/tests/Beam/AsyncTests.fs +++ b/tests/Beam/AsyncTests.fs @@ -5,8 +5,160 @@ open Fable.Tests.Util open Util.Testing open System.Threading +let private runOffThreadPool action = +#if FABLE_COMPILER + action () +#else + // .NET reschedules deferred FromContinuations callbacks invoked on a pool thread. + // Use a dedicated thread so the probe can observe completion and thrown callbacks. + let mutable failure = None + let thread = Thread(ThreadStart(fun () -> + try action () + with error -> failure <- Some error)) + thread.Start() + thread.Join() + failure |> Option.iter raise +#endif + +let private observeTryCancelled token (work: Async) = + let calls = ResizeArray() + Async.StartWithContinuations( + work, + (fun value -> calls.Add(string value)), + (fun error -> calls.Add("error: " + error.Message)), + (fun error -> calls.Add("cancel: " + error.Message)), + token) + calls + let private raiseCaughtCancellation (error: exn) : unit = raise error +[] +let ``test TryCancelled preserves success and ordinary errors`` () = + let mutable compensations = 0 + let wrap work = Async.TryCancelled(work, fun _ -> compensations <- compensations + 1) + equal [|"42"|] ((observeTryCancelled CancellationToken.None (wrap (async.Return 42))).ToArray()) + for error in [Exception "body"; OperationCanceledException "thrown"] do + let work: Async = async { return raise error } + equal [|"error: " + error.Message|] ((observeTryCancelled CancellationToken.None (wrap work)).ToArray()) + let mutable completed = false + Async.StartWithContinuations( + Async.TryCancelled(async.Return (), fun _ -> compensations <- compensations + 1), + (fun () -> completed <- true), raise, raise) + equal true completed + equal 0 compensations + +[] +let ``test TryCancelled preserves cancellation identity and supports reuse`` () = + for throws in [false; true] do + let original = OperationCanceledException "original" + let calls = ResizeArray() + let mutable handlerSawOriginal = true + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel original) + let work = Async.TryCancelled(body, fun error -> + handlerSawOriginal <- handlerSawOriginal && obj.ReferenceEquals(original, error) + calls.Add "compensation" + if throws then failwith "compensation defect") + for _ in 1 .. 2 do + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), + (fun _ -> failwith "unexpected error"), + (fun error -> + equal true (obj.ReferenceEquals(original, error)) + calls.Add "cancel")) + equal [|"compensation"; "cancel"; "compensation"; "cancel"|] (calls.ToArray()) + equal true handlerSawOriginal + +[] +let ``test TryCancelled installs nested handlers before cancellation`` () = + use source = new CancellationTokenSource() + source.Cancel() + let mutable entered = false + let calls = ResizeArray() + let body = async { entered <- true; return 42 } + let inner = Async.TryCancelled(body, fun _ -> calls.Add "inner") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer") + Async.StartWithContinuations(outer, + (fun _ -> failwith "unexpected success"), raise, + (fun _ -> calls.Add "cancel"), source.Token) + equal false entered + equal [|"inner"; "outer"; "cancel"|] (calls.ToArray()) + +[] +let ``test TryCancelled handles deferred cancellation`` () = + runOffThreadPool (fun () -> + let original = OperationCanceledException "deferred" + let mutable cancel = fun (_: OperationCanceledException) -> failwith "not started" + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, continuation) -> cancel <- continuation) + let work = Async.TryCancelled(body, fun _ -> calls.Add "compensation") + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), raise, + (fun error -> equal true (obj.ReferenceEquals(original, error)); calls.Add "cancel")) + equal 0 calls.Count + cancel original + equal [|"compensation"; "cancel"|] (calls.ToArray())) + +[] +let ``test TryCancelled preserves finalizer order when handlers throw`` () = + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel (OperationCanceledException "original")) + let finalized = async.TryFinally(body, fun () -> calls.Add "finally") + let inner = Async.TryCancelled(finalized, fun _ -> calls.Add "inner"; failwith "inner defect") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer"; failwith "outer defect") + equal [|"cancel: original"|] ((observeTryCancelled CancellationToken.None outer).ToArray()) + equal [|"finally"; "inner"; "outer"|] (calls.ToArray()) + +[] +let ``test TryCancelled propagates downstream errors once`` () = + runOffThreadPool (fun () -> + for channel in ["success"; "error"; "cancel"] do + let mutable compensations = 0 + let calls = ResizeArray() + let mutable complete = fun () -> failwith "not started" + let body: Async = Async.FromContinuations(fun (ok, error, cancel) -> + complete <- fun () -> + match channel with + | "success" -> ok 42 + | "error" -> error (Exception "body") + | _ -> cancel (OperationCanceledException "original")) + let work = Async.TryCancelled(body, fun _ -> compensations <- compensations + 1) + let terminal name = calls.Add name; failwith "downstream" + Async.StartWithContinuations(work, + (fun _ -> terminal "success"), + (fun _ -> terminal "error"), + (fun _ -> terminal "cancel")) + let caught = + try + complete () + "no exception" + with error -> error.Message + equal "downstream" caught + equal [|channel|] (calls.ToArray()) + equal (if channel = "cancel" then 1 else 0) compensations) + +[] +let ``test TryCancelled preserves bounded trampoline execution`` () = + async { + let original = OperationCanceledException "deep cancellation" + let mutable compensations = 0 + let rec loop remaining = + if remaining = 0 then + Async.FromContinuations(fun (_, _, cancel) -> cancel original) + else + Async.TryCancelled(async { + do! async.Return () + return! loop (remaining - 1) + }, fun _ -> compensations <- compensations + 1) + do! Async.FromContinuations(fun (ok, error, _) -> + Async.StartWithContinuations(loop 10000, + (fun () -> error (Exception "unexpected success")), error, + (fun cancelled -> + if obj.ReferenceEquals(original, cancelled) then ok () + else error (Exception "cancellation identity changed")))) + equal 10000 compensations + } + |> Async.RunSynchronously + [] let ``test RunSynchronously optional timeout and cancellation token`` () = let work = async { return 42 } diff --git a/tests/Js/Main/AsyncTests.fs b/tests/Js/Main/AsyncTests.fs index 2ca07f8b86..77a9965250 100644 --- a/tests/Js/Main/AsyncTests.fs +++ b/tests/Js/Main/AsyncTests.fs @@ -1,9 +1,35 @@ module Fable.Tests.Async open System +open System.Threading open Util open Util.Testing +let private runOffThreadPool action = +#if FABLE_COMPILER + action () +#else + // .NET reschedules deferred FromContinuations callbacks invoked on a pool thread. + // Use a dedicated thread so the probe can observe completion and thrown callbacks. + let mutable failure = None + let thread = Thread(ThreadStart(fun () -> + try action () + with error -> failure <- Some error)) + thread.Start() + thread.Join() + failure |> Option.iter raise +#endif + +let private observeTryCancelled token (work: Async) = + let calls = ResizeArray() + Async.StartWithContinuations( + work, + (fun value -> calls.Add(string value)), + (fun error -> calls.Add("error: " + error.Message)), + (fun error -> calls.Add("cancel: " + error.Message)), + token) + calls + module private FinalizerRegressions = open System open System.Threading @@ -169,6 +195,124 @@ let private fromGenericCallback (callback: ('T -> unit) -> (exn -> unit) -> (Ope let tests = testList "Async" [ + testCase "TryCancelled preserves success and ordinary errors" <| fun () -> + let mutable compensations = 0 + let wrap work = Async.TryCancelled(work, fun _ -> compensations <- compensations + 1) + equal [|"42"|] ((observeTryCancelled CancellationToken.None (wrap (async.Return 42))).ToArray()) + for error in [Exception "body"; OperationCanceledException "thrown"] do + let work: Async = async { return raise error } + equal [|"error: " + error.Message|] ((observeTryCancelled CancellationToken.None (wrap work)).ToArray()) + let mutable completed = false + Async.StartWithContinuations( + Async.TryCancelled(async.Return (), fun _ -> compensations <- compensations + 1), + (fun () -> completed <- true), raise, raise) + equal true completed + equal 0 compensations + + testCase "TryCancelled preserves cancellation identity and supports reuse" <| fun () -> + for throws in [false; true] do + let original = OperationCanceledException "original" + let calls = ResizeArray() + let mutable handlerSawOriginal = true + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel original) + let work = Async.TryCancelled(body, fun error -> + handlerSawOriginal <- handlerSawOriginal && obj.ReferenceEquals(original, error) + calls.Add "compensation" + if throws then failwith "compensation defect") + for _ in 1 .. 2 do + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), + (fun _ -> failwith "unexpected error"), + (fun error -> + equal true (obj.ReferenceEquals(original, error)) + calls.Add "cancel")) + equal [|"compensation"; "cancel"; "compensation"; "cancel"|] (calls.ToArray()) + equal true handlerSawOriginal + + testCase "TryCancelled installs nested handlers before cancellation" <| fun () -> + use source = new CancellationTokenSource() + source.Cancel() + let mutable entered = false + let calls = ResizeArray() + let body = async { entered <- true; return 42 } + let inner = Async.TryCancelled(body, fun _ -> calls.Add "inner") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer") + Async.StartWithContinuations(outer, + (fun _ -> failwith "unexpected success"), raise, + (fun _ -> calls.Add "cancel"), source.Token) + equal false entered + equal [|"inner"; "outer"; "cancel"|] (calls.ToArray()) + + testCase "TryCancelled handles deferred cancellation" <| fun () -> + runOffThreadPool (fun () -> + let original = OperationCanceledException "deferred" + let mutable cancel = fun (_: OperationCanceledException) -> failwith "not started" + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, continuation) -> cancel <- continuation) + let work = Async.TryCancelled(body, fun _ -> calls.Add "compensation") + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), raise, + (fun error -> equal true (obj.ReferenceEquals(original, error)); calls.Add "cancel")) + equal 0 calls.Count + cancel original + equal [|"compensation"; "cancel"|] (calls.ToArray())) + + testCase "TryCancelled preserves finalizer order when handlers throw" <| fun () -> + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel (OperationCanceledException "original")) + let finalized = async.TryFinally(body, fun () -> calls.Add "finally") + let inner = Async.TryCancelled(finalized, fun _ -> calls.Add "inner"; failwith "inner defect") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer"; failwith "outer defect") + equal [|"cancel: original"|] ((observeTryCancelled CancellationToken.None outer).ToArray()) + equal [|"finally"; "inner"; "outer"|] (calls.ToArray()) + + testCase "TryCancelled propagates downstream errors once" <| fun () -> + runOffThreadPool (fun () -> + for channel in ["success"; "error"; "cancel"] do + let mutable compensations = 0 + let calls = ResizeArray() + let mutable complete = fun () -> failwith "not started" + let body: Async = Async.FromContinuations(fun (ok, error, cancel) -> + complete <- fun () -> + match channel with + | "success" -> ok 42 + | "error" -> error (Exception "body") + | _ -> cancel (OperationCanceledException "original")) + let work = Async.TryCancelled(body, fun _ -> compensations <- compensations + 1) + let terminal name = calls.Add name; failwith "downstream" + Async.StartWithContinuations(work, + (fun _ -> terminal "success"), + (fun _ -> terminal "error"), + (fun _ -> terminal "cancel")) + let caught = + try + complete () + "no exception" + with error -> error.Message + equal "downstream" caught + equal [|channel|] (calls.ToArray()) + equal (if channel = "cancel" then 1 else 0) compensations) + + testCaseAsync "TryCancelled preserves bounded trampoline execution" <| fun () -> async { + let original = OperationCanceledException "deep cancellation" + let mutable compensations = 0 + let rec loop remaining = + if remaining = 0 then + Async.FromContinuations(fun (_, _, cancel) -> cancel original) + else + Async.TryCancelled(async { + do! async.Return () + return! loop (remaining - 1) + }, fun _ -> compensations <- compensations + 1) + do! Async.FromContinuations(fun (ok, error, _) -> + Async.StartWithContinuations(loop 10000, + (fun () -> error (Exception "unexpected success")), error, + (fun cancelled -> + if obj.ReferenceEquals(original, cancelled) then ok () + else error (Exception "cancellation identity changed")))) + equal 10000 compensations + } + testCase "TryFinally installs cleanup before pre-cancellation" <| FinalizerRegressions.preCancelled testCase "Pre-cancelled nested TryFinally only runs outer cleanup" <| FinalizerRegressions.nestedPreCancelled testCase "Cancelled acquisition disposes use and use bang resources" <| FinalizerRegressions.cancelledAcquisition diff --git a/tests/Python/TestAsync.fs b/tests/Python/TestAsync.fs index b2c1755ecd..6d8cd7002d 100644 --- a/tests/Python/TestAsync.fs +++ b/tests/Python/TestAsync.fs @@ -1,9 +1,35 @@ module Fable.Tests.Async open System +open System.Threading open Util open Util.Testing +let private runOffThreadPool action = +#if FABLE_COMPILER + action () +#else + // .NET reschedules deferred FromContinuations callbacks invoked on a pool thread. + // Use a dedicated thread so the probe can observe completion and thrown callbacks. + let mutable failure = None + let thread = Thread(ThreadStart(fun () -> + try action () + with error -> failure <- Some error)) + thread.Start() + thread.Join() + failure |> Option.iter raise +#endif + +let private observeTryCancelled token (work: Async) = + let calls = ResizeArray() + Async.StartWithContinuations( + work, + (fun value -> calls.Add(string value)), + (fun error -> calls.Add("error: " + error.Message)), + (fun error -> calls.Add("cancel: " + error.Message)), + token) + calls + module private FinalizerRegressions = open System open System.Threading @@ -164,6 +190,133 @@ let errorWork: Async = Async.FromContinuations(fun (_,onError,_) -> onEr let cancelWork: Async = Async.FromContinuations(fun (_,_,onCancel) -> System.OperationCanceledException("cancelled") |> onCancel) +[] +let ``test TryCancelled preserves success and ordinary errors`` () = + let mutable compensations = 0 + let wrap work = Async.TryCancelled(work, fun _ -> compensations <- compensations + 1) + equal [|"42"|] ((observeTryCancelled CancellationToken.None (wrap (async.Return 42))).ToArray()) + for error in [Exception "body"; OperationCanceledException "thrown"] do + let work: Async = async { return raise error } + equal [|"error: " + error.Message|] ((observeTryCancelled CancellationToken.None (wrap work)).ToArray()) + let mutable completed = false + Async.StartWithContinuations( + Async.TryCancelled(async.Return (), fun _ -> compensations <- compensations + 1), + (fun () -> completed <- true), raise, raise) + equal true completed + equal 0 compensations + +[] +let ``test TryCancelled preserves cancellation identity and supports reuse`` () = + for throws in [false; true] do + let original = OperationCanceledException "original" + let calls = ResizeArray() + let mutable handlerSawOriginal = true + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel original) + let work = Async.TryCancelled(body, fun error -> + handlerSawOriginal <- handlerSawOriginal && obj.ReferenceEquals(original, error) + calls.Add "compensation" + if throws then failwith "compensation defect") + for _ in 1 .. 2 do + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), + (fun _ -> failwith "unexpected error"), + (fun error -> + equal true (obj.ReferenceEquals(original, error)) + calls.Add "cancel")) + equal [|"compensation"; "cancel"; "compensation"; "cancel"|] (calls.ToArray()) + equal true handlerSawOriginal + +[] +let ``test TryCancelled installs nested handlers before cancellation`` () = + use source = new CancellationTokenSource() + source.Cancel() + let mutable entered = false + let calls = ResizeArray() + let body = async { entered <- true; return 42 } + let inner = Async.TryCancelled(body, fun _ -> calls.Add "inner") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer") + Async.StartWithContinuations(outer, + (fun _ -> failwith "unexpected success"), raise, + (fun _ -> calls.Add "cancel"), source.Token) + equal false entered + equal [|"inner"; "outer"; "cancel"|] (calls.ToArray()) + +[] +let ``test TryCancelled handles deferred cancellation`` () = + runOffThreadPool (fun () -> + let original = OperationCanceledException "deferred" + let mutable cancel = fun (_: OperationCanceledException) -> failwith "not started" + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, continuation) -> cancel <- continuation) + let work = Async.TryCancelled(body, fun _ -> calls.Add "compensation") + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), raise, + (fun error -> equal true (obj.ReferenceEquals(original, error)); calls.Add "cancel")) + equal 0 calls.Count + cancel original + equal [|"compensation"; "cancel"|] (calls.ToArray())) + +[] +let ``test TryCancelled preserves finalizer order when handlers throw`` () = + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel (OperationCanceledException "original")) + let finalized = async.TryFinally(body, fun () -> calls.Add "finally") + let inner = Async.TryCancelled(finalized, fun _ -> calls.Add "inner"; failwith "inner defect") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer"; failwith "outer defect") + equal [|"cancel: original"|] ((observeTryCancelled CancellationToken.None outer).ToArray()) + equal [|"finally"; "inner"; "outer"|] (calls.ToArray()) + +[] +let ``test TryCancelled propagates downstream errors once`` () = + runOffThreadPool (fun () -> + for channel in ["success"; "error"; "cancel"] do + let mutable compensations = 0 + let calls = ResizeArray() + let mutable complete = fun () -> failwith "not started" + let body: Async = Async.FromContinuations(fun (ok, error, cancel) -> + complete <- fun () -> + match channel with + | "success" -> ok 42 + | "error" -> error (Exception "body") + | _ -> cancel (OperationCanceledException "original")) + let work = Async.TryCancelled(body, fun _ -> compensations <- compensations + 1) + let terminal name = calls.Add name; failwith "downstream" + Async.StartWithContinuations(work, + (fun _ -> terminal "success"), + (fun _ -> terminal "error"), + (fun _ -> terminal "cancel")) + let caught = + try + complete () + "no exception" + with error -> error.Message + equal "downstream" caught + equal [|channel|] (calls.ToArray()) + equal (if channel = "cancel" then 1 else 0) compensations) + +[] +let ``test TryCancelled preserves bounded trampoline execution`` () = + async { + let original = OperationCanceledException "deep cancellation" + let mutable compensations = 0 + let rec loop remaining = + if remaining = 0 then + Async.FromContinuations(fun (_, _, cancel) -> cancel original) + else + Async.TryCancelled(async { + do! async.Return () + return! loop (remaining - 1) + }, fun _ -> compensations <- compensations + 1) + do! Async.FromContinuations(fun (ok, error, _) -> + Async.StartWithContinuations(loop 10000, + (fun () -> error (Exception "unexpected success")), error, + (fun cancelled -> + if obj.ReferenceEquals(original, cancelled) then ok () + else error (Exception "cancellation identity changed")))) + equal 10000 compensations + } + |> Async.RunSynchronously + [] let ``test Simple async translates without exception`` () = async { return () } diff --git a/tests/Python/TestType.fs b/tests/Python/TestType.fs index 29e5416dcb..fc8d31ad32 100644 --- a/tests/Python/TestType.fs +++ b/tests/Python/TestType.fs @@ -772,6 +772,14 @@ let ``test Optional named params work`` () = StaticClass.NamedParams(foo="14") |> equal 11 StaticClass.NamedParams() |> equal "John is 30 years old" +[] +let ``test Mutable function bindings accept replacement callbacks`` () = + let mutable callback = fun (initial: int) -> initial + 1 + let replace (replacement: int -> int) = callback <- replacement + callback 3 |> equal 4 + replace (fun differentName -> differentName * 2) + callback 3 |> equal 6 + [] let ``test F# optional param works`` () = let mutable f1 = fun (v: bool) ->