diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.StateTransitionBug.Tests.CE.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.StateTransitionBug.Tests.CE.fs index d2eb0e9b..37746f05 100644 --- a/src/FSharpy.TaskSeq.Test/TaskSeq.StateTransitionBug.Tests.CE.fs +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.StateTransitionBug.Tests.CE.fs @@ -48,6 +48,10 @@ let ``CE empty taskSeq, GetAsyncEnumerator multiple times and then MoveNextAsyn do! Assert.moveNextAndCheck false enumerator } +// FIXED! +// Previously: shaky test. Appears that this occasionally raises a NullReferenceException, +// esp when there's stress (i.e. run all at the same time). +// See https://github.com/abelbraaksma/TaskSeq/pull/54 [)>] let ``CE empty taskSeq, GetAsyncEnumerator + MoveNextAsync multiple times`` variant = task { let tskSeq = Gen.getEmptyVariant variant @@ -60,6 +64,25 @@ let ``CE empty taskSeq, GetAsyncEnumerator + MoveNextAsync multiple times`` vari do! Assert.moveNextAndCheck false enumerator2 // new hone should also work without raising } +// FIXED! +// This is the simpler version of the above test. +[] +let ``BUG #54 CE with empty taskSeq and Delay, crash after GetAsyncEnumerator + MoveNextAsync 2x`` () = task { + // See: https://github.com/abelbraaksma/TaskSeq/pull/54 + let tskSeq = taskSeq { do! Task.Delay(50) |> Task.ofTask } + + use enumerator1 = tskSeq.GetAsyncEnumerator() + let! (hasNext: bool) = enumerator1.MoveNextAsync() + hasNext |> should be False + + use enumerator1 = tskSeq.GetAsyncEnumerator() + let! (hasNext: bool) = enumerator1.MoveNextAsync() + hasNext |> should be False + + let! (hasNext: bool) = enumerator1.MoveNextAsync() // fail here + hasNext |> should be False +} + [)>] let ``CE empty taskSeq, GetAsyncEnumerator + MoveNextAsync 100x in a loop`` variant = task { let tskSeq = Gen.getEmptyVariant variant diff --git a/src/FSharpy.TaskSeq.Test/Traces/TRACE_FAIL 'CE empty taskSeq, GetAsyncEnumerator + MoveNextAsync multiple times'.txt b/src/FSharpy.TaskSeq.Test/Traces/TRACE_FAIL 'CE empty taskSeq, GetAsyncEnumerator + MoveNextAsync multiple times'.txt new file mode 100644 index 00000000..86cf34a7 --- /dev/null +++ b/src/FSharpy.TaskSeq.Test/Traces/TRACE_FAIL 'CE empty taskSeq, GetAsyncEnumerator + MoveNextAsync multiple times'.txt @@ -0,0 +1,56 @@ +at AfterCode<_, _>, after F# inits the sm, and we can attach extra info +MoveNextAsync... +at MoveNextAsync: normal resumption scenario +at MoveNextAsync: start calling builder.MoveNext() +Resuming at resumption point 0 +at Run.MoveNext start +at Bind +at Bind: with __stack_fin = false +at Bind: calling AwaitUnsafeOnCompleted +at Run.MoveNext, __stack_code_fin=False +at Run.MoveNext, await +at MoveNextAsync: done calling builder.MoveNext() +at MoveNextAsyncResult: case pending/faulted/cancelled... +at Bind: with __stack_fin = true +at Bind: with getting result from awaiter +at Bind: calling continuation +at Bind +at Bind: with __stack_fin = false +at Bind: calling AwaitUnsafeOnCompleted +at Run.MoveNext, __stack_code_fin=False +at Run.MoveNext, await +at Bind: with __stack_fin = true +at Bind: with getting result from awaiter +at Bind: calling continuation +at Bind +at Bind: with __stack_fin = false +at Bind: calling AwaitUnsafeOnCompleted +at Run.MoveNext, __stack_code_fin=False +at Run.MoveNext, await +at Bind: with __stack_fin = true +at Bind: with getting result from awaiter +at Bind: calling continuation +at Zero() +at Run.MoveNext, __stack_code_fin=True +at Run.MoveNext, done +Getting result for token on 'None' branch, status: Succeeded +GetAsyncEnumerator, cloning... +MoveNextAsync... +at MoveNextAsync: normal resumption scenario +at MoveNextAsync: start calling builder.MoveNext() +at Bind: with __stack_fin = true +at Bind: with getting result from awaiter +Setting exception of PromiseOfValueOrEnd to: Object reference not set to an instance of an object. +at MoveNextAsync: done calling builder.MoveNext() +at MoveNextAsyncResult: case pending/faulted/cancelled... +Getting result for token on 'None' branch, status: Faulted +Error 'Object reference not set to an instance of an object.' for token: 2 +DisposeAsync... +DisposeAsync... +Setting exception of PromiseOfValueOrEnd to: An attempt was made to transition a task to a final state when it had already completed. +System.NullReferenceException: Object reference not set to an instance of an object. + at FSharpy.Tests.Bug #42 -- synchronous.CE empty taskSeq\, GetAsyncEnumerator - MoveNextAsync multiple times@54.MoveNext() in D:\Projects\OpenSource\Abel\TaskSeq\src\FSharpy.TaskSeq.Test\TaskSeq.StateTransitionBug.Tests.CE.fs:line 62 + at Xunit.Sdk.TestInvoker`1.<>c__DisplayClass48_0.<b__1>d.MoveNext() in /_/src/xunit.execution/Sdk/Frameworks/Runners/TestInvoker.cs:line 264 +--- End of stack trace from previous location --- + at Xunit.Sdk.ExecutionTimer.AggregateAsync(Func`1 asyncAction) in /_/src/xunit.execution/Sdk/Frameworks/ExecutionTimer.cs:line 48 + at Xunit.Sdk.ExceptionAggregator.RunAsync(Func`1 code) in /_/src/xunit.core/Sdk/ExceptionAggregator.cs:line 90 \ No newline at end of file diff --git a/src/FSharpy.TaskSeq.Test/Traces/TRACE_SUCCESS 'CE empty taskSeq, GetAsyncEnumerator + MoveNextAsync multiple times'.txt b/src/FSharpy.TaskSeq.Test/Traces/TRACE_SUCCESS 'CE empty taskSeq, GetAsyncEnumerator + MoveNextAsync multiple times'.txt new file mode 100644 index 00000000..c6075499 --- /dev/null +++ b/src/FSharpy.TaskSeq.Test/Traces/TRACE_SUCCESS 'CE empty taskSeq, GetAsyncEnumerator + MoveNextAsync multiple times'.txt @@ -0,0 +1,76 @@ +at AfterCode<_, _>, after F# inits the sm, and we can attach extra info +MoveNextAsync... +at MoveNextAsync: normal resumption scenario +at MoveNextAsync: start calling builder.MoveNext() +Resuming at resumption point 0 +at Run.MoveNext start +at Bind +at Bind: with __stack_fin = false +at Bind: calling AwaitUnsafeOnCompleted +at Run.MoveNext, __stack_code_fin=False +at Run.MoveNext, await +at MoveNextAsync: done calling builder.MoveNext() +at MoveNextAsyncResult: case pending/faulted/cancelled... +at Bind: with __stack_fin = true +at Bind: with getting result from awaiter +at Bind: calling continuation +at Bind +at Bind: with __stack_fin = false +at Bind: calling AwaitUnsafeOnCompleted +at Run.MoveNext, __stack_code_fin=False +at Run.MoveNext, await +at Bind: with __stack_fin = true +at Bind: with getting result from awaiter +at Bind: calling continuation +at Bind +at Bind: with __stack_fin = false +at Bind: calling AwaitUnsafeOnCompleted +at Run.MoveNext, __stack_code_fin=False +at Run.MoveNext, await +at Bind: with __stack_fin = true +at Bind: with getting result from awaiter +at Bind: calling continuation +at Zero() +at Run.MoveNext, __stack_code_fin=True +at Run.MoveNext, done +Getting result for token on 'None' branch, status: Succeeded +GetAsyncEnumerator, cloning... +MoveNextAsync... +at MoveNextAsync: completed = true +MoveNextAsync... +at MoveNextAsync: normal resumption scenario +at MoveNextAsync: start calling builder.MoveNext() +Resuming at resumption point 0 +at Run.MoveNext start +at Bind +at Bind: with __stack_fin = false +at Bind: calling AwaitUnsafeOnCompleted +at Run.MoveNext, __stack_code_fin=False +at Run.MoveNext, await +at MoveNextAsync: done calling builder.MoveNext() +at MoveNextAsyncResult: case pending/faulted/cancelled... +at Bind: with __stack_fin = true +at Bind: with getting result from awaiter +at Bind: calling continuation +at Bind +at Bind: with __stack_fin = false +at Bind: calling AwaitUnsafeOnCompleted +at Run.MoveNext, __stack_code_fin=False +at Run.MoveNext, await +at Bind: with __stack_fin = true +at Bind: with getting result from awaiter +at Bind: calling continuation +at Bind +at Bind: with __stack_fin = false +at Bind: calling AwaitUnsafeOnCompleted +at Run.MoveNext, __stack_code_fin=False +at Run.MoveNext, await +at Bind: with __stack_fin = true +at Bind: with getting result from awaiter +at Bind: calling continuation +at Zero() +at Run.MoveNext, __stack_code_fin=True +at Run.MoveNext, done +Getting result for token on 'None' branch, status: Succeeded +DisposeAsync... +DisposeAsync... \ No newline at end of file diff --git a/src/FSharpy.TaskSeq.Test/fail-trace.txt b/src/FSharpy.TaskSeq.Test/fail-trace.txt new file mode 100644 index 00000000..b9842eab --- /dev/null +++ b/src/FSharpy.TaskSeq.Test/fail-trace.txt @@ -0,0 +1,60 @@ +6 (false): at AfterCode<_, _>, after F# inits the sm, and we can attach extra info +6 (false): GetAsyncEnumerator, start cloning... +6 (false): GetAsyncEnumerator, finished cloning... +6 (false): MoveNextAsync... +6 (false): at MoveNextAsync: normal resumption scenario +6 (false): at MoveNextAsync: start calling builder.MoveNext() +6 (false): at IAsyncStateMatchine.MoveNext +6 (false): Resuming at resumption point 0 +6 (false): at Run.MoveNext start +6 (false): at Bind +6 (false): at Bind: with __stack_fin = false +6 (false): at Bind: calling AwaitUnsafeOnCompleted +6 (false): at Run.MoveNext, __stack_code_fin=False +6 (false): at Run.MoveNext, await +6 (false): at MoveNextAsync: done calling builder.MoveNext() +6 (false): at MoveNextAsyncResult: case Pending... +13 (false): at IAsyncStateMatchine.MoveNext +13 (false): at Bind: with __stack_fin = true +13 (false): at Bind: with getting result from awaiter +13 (false): at Bind: calling continuation +13 (false): at Zero() +13 (false): at Run.MoveNext, __stack_code_fin=True +13 (false): at Run.MoveNext, done +14 (false): Getting result for token on 'None' branch, status: Succeeded +15 (false): GetAsyncEnumerator, start cloning... +15 (false): GetAsyncEnumerator, finished cloning... +15 (false): MoveNextAsync... +15 (false): at MoveNextAsync: normal resumption scenario +15 (false): at MoveNextAsync: start calling builder.MoveNext() +15 (false): at IAsyncStateMatchine.MoveNext +15 (false): at Bind: with __stack_fin = true +15 (false): at Bind: with getting result from awaiter +15 (false): Exception dump: +15 (false): System.NullReferenceException: Object reference not set to an instance of an object. + at FSharpy.Tests.TestUtils.Gen.getEmptyVariant@308-15.MoveNext() in D:\Projects\OpenSource\Abel\TaskSeq\src\FSharpy.TaskSeq.Test\TestUtils.fs:line 309 +15 (false): Setting exception of PromiseOfValueOrEnd to: Object reference not set to an instance of an object. +15 (false): at MoveNextAsync: done calling builder.MoveNext() +15 (false): at MoveNextAsyncResult: case Faulted... +15 (false): Getting result for token on 'None' branch, status: Faulted +15 (false): Error 'Object reference not set to an instance of an object.' for token: 3 +15 (false): DisposeAsync... +15 (false): DisposeAsync... +13 (false): Exception dump: +13 (false): System.InvalidOperationException: An attempt was made to transition a task to a final state when it had already completed. + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.SetExistingTaskResult(Task`1 task, TResult result) + at System.Runtime.CompilerServices.AsyncIteratorMethodBuilder.Complete() + at FSharpy.Tests.TestUtils.Gen.getEmptyVariant@308-15.MoveNext() in D:\Projects\OpenSource\Abel\TaskSeq\src\FSharpy.TaskSeq.Test\TestUtils.fs:line 309 +13 (false): Setting exception of PromiseOfValueOrEnd to: An attempt was made to transition a task to a final state when it had already completed. +13 (false): at IAsyncStatemachine EXCEPTION!!! +13 (false): System.InvalidOperationException: Operation is not valid due to the current state of the object. + at System.Threading.Tasks.Sources.ManualResetValueTaskSourceCore`1.SignalCompletion() + at System.Threading.Tasks.Sources.ManualResetValueTaskSourceCore`1.SetException(Exception error) + at FSharpy.Tests.TestUtils.Gen.getEmptyVariant@308-15.MoveNext() in D:\Projects\OpenSource\Abel\TaskSeq\src\FSharpy.TaskSeq.Test\TestUtils.fs:line 309 + at FSharpy.TaskSeqBuilders.TaskSeq`2.System.Runtime.CompilerServices.IAsyncStateMachine.MoveNext() in D:\Projects\OpenSource\Abel\TaskSeq\src\FSharpy.TaskSeq\TaskSeqBuilder.fs:line 249 +System.NullReferenceException: Object reference not set to an instance of an object. + at FSharpy.Tests.Bug #42 -- synchronous.CE empty taskSeq\, GetAsyncEnumerator - MoveNextAsync multiple times@52.MoveNext() in D:\Projects\OpenSource\Abel\TaskSeq\src\FSharpy.TaskSeq.Test\TaskSeq.StateTransitionBug.Tests.CE.fs:line 60 + at Xunit.Sdk.TestInvoker`1.<>c__DisplayClass48_0.<b__1>d.MoveNext() in /_/src/xunit.execution/Sdk/Frameworks/Runners/TestInvoker.cs:line 264 +--- End of stack trace from previous location --- + at Xunit.Sdk.ExecutionTimer.AggregateAsync(Func`1 asyncAction) in /_/src/xunit.execution/Sdk/Frameworks/ExecutionTimer.cs:line 48 + at Xunit.Sdk.ExceptionAggregator.RunAsync(Func`1 code) in /_/src/xunit.core/Sdk/ExceptionAggregator.cs:line 90 \ No newline at end of file diff --git a/src/FSharpy.TaskSeq.Test/success-trace.txt b/src/FSharpy.TaskSeq.Test/success-trace.txt new file mode 100644 index 00000000..e7571168 --- /dev/null +++ b/src/FSharpy.TaskSeq.Test/success-trace.txt @@ -0,0 +1,51 @@ +6 (false): at AfterCode<_, _>, after F# inits the sm, and we can attach extra info +6 (false): GetAsyncEnumerator, start cloning... +6 (false): GetAsyncEnumerator, finished cloning... +6 (false): MoveNextAsync... +6 (false): at MoveNextAsync: normal resumption scenario +6 (false): at MoveNextAsync: start calling builder.MoveNext() +6 (false): at IAsyncStateMatchine.MoveNext +6 (false): Resuming at resumption point 0 +6 (false): at Run.MoveNext start +6 (false): at Bind +6 (false): at Bind: with __stack_fin = false +6 (false): at Bind: calling AwaitUnsafeOnCompleted +6 (false): at Run.MoveNext, __stack_code_fin=False +6 (false): at Run.MoveNext, await +6 (false): at MoveNextAsync: done calling builder.MoveNext() +6 (false): at MoveNextAsyncResult: case Pending... +13 (false): at IAsyncStateMatchine.MoveNext +13 (false): at Bind: with __stack_fin = true +13 (false): at Bind: with getting result from awaiter +13 (false): at Bind: calling continuation +13 (false): at Zero() +13 (false): at Run.MoveNext, __stack_code_fin=True +13 (false): at Run.MoveNext, done +14 (false): Getting result for token on 'None' branch, status: Succeeded +15 (false): GetAsyncEnumerator, start cloning... +15 (false): GetAsyncEnumerator, finished cloning... +15 (false): MoveNextAsync... +15 (false): at MoveNextAsync: completed = true +15 (false): MoveNextAsync... +15 (false): at MoveNextAsync: normal resumption scenario +15 (false): at MoveNextAsync: start calling builder.MoveNext() +15 (false): at IAsyncStateMatchine.MoveNext +15 (false): Resuming at resumption point 0 +15 (false): at Run.MoveNext start +15 (false): at Bind +15 (false): at Bind: with __stack_fin = false +15 (false): at Bind: calling AwaitUnsafeOnCompleted +15 (false): at Run.MoveNext, __stack_code_fin=False +15 (false): at Run.MoveNext, await +15 (false): at MoveNextAsync: done calling builder.MoveNext() +15 (false): at MoveNextAsyncResult: case Pending... +9 (true): at IAsyncStateMatchine.MoveNext +9 (true): at Bind: with __stack_fin = true +9 (true): at Bind: with getting result from awaiter +9 (true): at Bind: calling continuation +9 (true): at Zero() +9 (true): at Run.MoveNext, __stack_code_fin=True +9 (true): at Run.MoveNext, done +9 (true): Getting result for token on 'None' branch, status: Succeeded +9 (true): DisposeAsync... +9 (true): DisposeAsync... diff --git a/src/FSharpy.TaskSeq/TaskSeqBuilder.fs b/src/FSharpy.TaskSeq/TaskSeqBuilder.fs index 5a7430e1..b8d7fd6d 100644 --- a/src/FSharpy.TaskSeq/TaskSeqBuilder.fs +++ b/src/FSharpy.TaskSeq/TaskSeqBuilder.fs @@ -161,50 +161,43 @@ and [] TaskSeq<'Machine, 'T // Note: Not entirely clear if this is needed, everything still compiles without it interface IValueTaskSource with - member this.GetResult(token: int16) = - this._machine.Data.promiseOfValueOrEnd.GetResult(token) - |> ignore + member this.GetResult token = + let canMoveNext = this._machine.Data.promiseOfValueOrEnd.GetResult token - member this.GetStatus(token: int16) = this._machine.Data.promiseOfValueOrEnd.GetStatus(token) + if not canMoveNext then + // see below in generic version for explanation + this._machine.Data.completed <- true + + member this.GetStatus token = this._machine.Data.promiseOfValueOrEnd.GetStatus token member this.OnCompleted(continuation, state, token, flags) = this._machine.Data.promiseOfValueOrEnd.OnCompleted(continuation, state, token, flags) - // Needed for MoveNextAsync to return a ValueTask + // Needed for MoveNextAsync to return a ValueTask, this manages the source of the ValueTask + // in combination with the ManualResetValueTaskSourceCore (in promiseOfValueOrEnd). interface IValueTaskSource with - member this.GetStatus(token: int16) = this._machine.Data.promiseOfValueOrEnd.GetStatus(token) + member this.GetStatus token = this._machine.Data.promiseOfValueOrEnd.GetStatus token - member this.GetResult(token: int16) = - try - log - "Getting result for token on normal branch, status: %A" - (this._machine.Data.promiseOfValueOrEnd.GetStatus(token)) + /// Returning the boolean value that is used as a result for MoveNextAsync() + member this.GetResult token = + let canMoveNext = this._machine.Data.promiseOfValueOrEnd.GetResult token - let x = this._machine.Data.promiseOfValueOrEnd.GetResult(token) - this._machine.Data.promiseOfValueOrEnd.Reset() - x - with e -> - // FYI: an exception here is usually caused by the CE statement (user code) throwing an exception - // We're just logging here because the following error would also be caught right here: - // "An attempt was made to transition a task to a final state when it had already completed." - log "Error '%s' for token: %i" e.Message token - reraise () + // This ensures that, esp. in cases where there's no actual iteration (i.e. empty seq) + // we can still detect completeness and prevent an incorrect jump in the resumable code. + // See https://github.com/abelbraaksma/TaskSeq/pull/54 + if not canMoveNext then + // Signal we reached the end. + // DO NOT call Data.builder.Complete() here, ONLY do that in the Run method. + this._machine.Data.completed <- true + canMoveNext member this.OnCompleted(continuation, state, token, flags) = this._machine.Data.promiseOfValueOrEnd.OnCompleted(continuation, state, token, flags) interface IAsyncStateMachine with /// The MoveNext method is called by builder.MoveNext() in the resumable code - member this.MoveNext() = - log "at IAsyncStateMatchine.MoveNext" - - try - moveNextRef &this._machine - with e -> - log "at IAsyncStatemachine EXCEPTION!!!" - log "%A" e - + member this.MoveNext() = moveNextRef &this._machine /// SetStatemachine is (currently) never called member _.SetStateMachine(_state) = () // not needed for reference type @@ -213,30 +206,30 @@ and [] TaskSeq<'Machine, 'T member this.GetAsyncEnumerator(ct) = // if this is null, it means it's the first time for this Enumerable to create an Enumerator // so, to prevent extra allocations, we just return 'self', with the iterator vars set appropriately. - //match this._machine.Data :> obj with - //| null when initialThreadId = Environment.CurrentManagedThreadId -> - // this.InitMachineData(ct, &this._machine) - // this // just return 'self' here - - //| _ -> - log "GetAsyncEnumerator, start cloning..." - - // We need to reset state, but only to the "initial machine", resetting the _machine to - // Unchecked.defaultof<_> is wrong, as the compiler uses this to track state. However, - // we do need a zeroed ResumptionPoint, otherwise we would continue after the last iteration - // returning an empty sequence. - // - // Solution: we shadow the initial machine, which we then re-assign here: - // - let clone = TaskSeq<'Machine, 'T>() // we used MemberwiseClone, TODO: test difference in perf, but this should be faster - - // _machine will change, _initialMachine will not, which can be used in a new clone. - // we still need to copy _initialMachine, as it has been initialized by the F# compiler in AfterCode<_, _>. - clone._machine <- this._initialMachine - clone._initialMachine <- this._initialMachine // TODO: proof with a test that this is necessary: probably not - clone.InitMachineData(ct, &clone._machine) - log "GetAsyncEnumerator, finished cloning..." - clone + match this._machine.Data :> obj with + | null when initialThreadId = Environment.CurrentManagedThreadId -> + this.InitMachineData(ct, &this._machine) + this // just return 'self' here + + | _ -> + log "GetAsyncEnumerator, start cloning..." + + // We need to reset state, but only to the "initial machine", resetting the _machine to + // Unchecked.defaultof<_> is wrong, as the compiler uses this to track state. However, + // we do need a zeroed ResumptionPoint, otherwise we would continue after the last iteration + // returning an empty sequence. + // + // Solution: we shadow the initial machine, which we then re-assign here: + // + let clone = TaskSeq<'Machine, 'T>() // we used MemberwiseClone, TODO: test difference in perf, but this should be faster + + // _machine will change, _initialMachine will not, which can be used in a new clone. + // we still need to copy _initialMachine, as it has been initialized by the F# compiler in AfterCode<_, _>. + clone._machine <- this._initialMachine + clone._initialMachine <- this._initialMachine // TODO: proof with a test that this is necessary: probably not + clone.InitMachineData(ct, &clone._machine) + log "GetAsyncEnumerator, finished cloning..." + clone interface System.Collections.Generic.IAsyncEnumerator<'T> with member this.Current = @@ -350,30 +343,31 @@ type TaskSeqBuilder() = //-- RESUMABLE CODE START __resumeAt sm.ResumptionPoint - log "Resuming at resumption point %i" sm.ResumptionPoint - try log "at Run.MoveNext start" let __stack_code_fin = code.Invoke(&sm) - log $"at Run.MoveNext, __stack_code_fin={__stack_code_fin}" - if __stack_code_fin then log $"at Run.MoveNext, done" + // Signal we're at the end + // NOTE: if we don't do it here, as well as in IValueTaskSource.GetResult + // we either end up in an endless loop, or we'll get NRE on empty sequences. + // see: https://github.com/abelbraaksma/TaskSeq/pull/54 sm.Data.promiseOfValueOrEnd.SetResult(false) sm.Data.builder.Complete() sm.Data.completed <- true elif sm.Data.current.IsSome then - log $"at Run.MoveNext, yield" + log $"at Run.MoveNext, still more items in enumerator" + // Signal there's more data: sm.Data.promiseOfValueOrEnd.SetResult(true) else // Goto request - log $"at Run.MoveNext, await" + log $"at Run.MoveNext, await, MoveNextAsync has not completed yet" // don't capture the full object in the next closure (won't work because: byref) // but only a reference to itself. @@ -386,12 +380,10 @@ type TaskSeqBuilder() = ) with exn -> - log "Exception dump:" - log "%A" exn log "Setting exception of PromiseOfValueOrEnd to: %s" exn.Message - sm.Data.promiseOfValueOrEnd.SetException(exn) sm.Data.builder.Complete() + //-- RESUMABLE CODE END )) (SetStateMachineMethodImpl<_>(fun sm state -> ())) // not used in reference impl @@ -403,16 +395,16 @@ type TaskSeqBuilder() = ts._machine <- sm ts :> IAsyncEnumerable<'T>)) else + // let initialResumptionFunc = TaskSeqResumptionFunc<'T>(fun sm -> code.Invoke(&sm)) + // let resumptionFuncExecutor = TaskSeqResumptionExecutor<'T>(fun sm f -> + // // TODO: add exception handling? + // if f.Invoke(&sm) then + // sm.ResumptionPoint <- -2) + // let setStateMachine = SetStateMachineMethodImpl<_>(fun sm f -> ()) + // sm.Machine.ResumptionFuncInfo <- (initialResumptionFunc, resumptionFuncExecutor, setStateMachine) + //sm.Start() NotImplementedException "No dynamic implementation for TaskSeq yet." |> raise - // let initialResumptionFunc = TaskSeqResumptionFunc<'T>(fun sm -> code.Invoke(&sm)) - // let resumptionFuncExecutor = TaskSeqResumptionExecutor<'T>(fun sm f -> - // // TODO: add exception handling? - // if f.Invoke(&sm) then - // sm.ResumptionPoint <- -2) - // let setStateMachine = SetStateMachineMethodImpl<_>(fun sm f -> ()) - // sm.Machine.ResumptionFuncInfo <- (initialResumptionFunc, resumptionFuncExecutor, setStateMachine) - //sm.Start() member inline _.Zero() : TaskSeqCode<'T> = @@ -452,8 +444,6 @@ type TaskSeqBuilder() = let __stack_yield_fin = ResumableCode.Yield().Invoke(&sm) __stack_condition_fin <- __stack_yield_fin - log "at WhileAsync: after Yield().Invoke(sm), __stack_condition_fin=%b" __stack_condition_fin - if __stack_condition_fin then condition_res <- task.Result else @@ -467,9 +457,11 @@ type TaskSeqBuilder() = ) member inline b.While([] condition: unit -> bool, body: TaskSeqCode<'T>) : TaskSeqCode<'T> = - log "at While(...), calling WhileAsync()" + log "at While(...)" - b.WhileAsync((fun () -> ValueTask(condition ())), body) + // was this: + // b.WhileAsync((fun () -> ValueTask(condition ())), body) + ResumableCode.While(condition, body) member inline _.TryWith(body: TaskSeqCode<'T>, catch: exn -> TaskSeqCode<'T>) : TaskSeqCode<'T> = ResumableCode.TryWith(body, catch) @@ -573,12 +565,9 @@ type TaskSeqBuilder() = TaskSeqCode<'T>(fun sm -> // This will yield with __stack_fin = false // This will resume with __stack_fin = true - log "at Yield, before Yield().Invoke(sm)" + log "at Yield" let __stack_fin = ResumableCode.Yield().Invoke(&sm) - - log "at Yield, __stack_fin = %b" __stack_fin - sm.Data.current <- ValueSome v sm.Data.awaiter <- null __stack_fin) @@ -605,30 +594,9 @@ type TaskSeqBuilder() = log "at Bind: this.completed = %b" sm.Data.completed if __stack_fin then - let uninitialized = Unchecked.defaultof> - log "at Bind: awaiter is initialized: %b" (uninitialized <> awaiter) - - if uninitialized = awaiter then - log "at Bind: task is null: %b" (isNull task) - awaiter <- task.GetAwaiter() - - log "at Bind: awaiter is initialized: %b" (uninitialized <> awaiter) - log "at Bind: with getting result from awaiter, completed: %b" awaiter.IsCompleted + let result = awaiter.GetResult() + (continuation result).Invoke(&sm) - let result = - try - awaiter.GetResult() - with e -> - log "at Bind: EXCEPTION getting result from awaiter: %A" e - Unchecked.defaultof<_> - - log "at Bind: calling continuation" - - try - (continuation result).Invoke(&sm) - with e -> - printfn "Exception!!! %s" e.Message - reraise () else log "at Bind: calling AwaitUnsafeOnCompleted"