Skip to content

Commit c603878

Browse files
feat: implement dynamic resumable code for taskSeq/taskSeqDynamic, fixes #246
Adds TaskSeqDynamicInfo<'T> and TaskSeqDynamic<'T> types that implement the dynamic (ResumptionDynamicInfo-based) execution path for taskSeq computation expressions. This fixes the NotImplementedException raised when taskSeq is used in contexts where the F# compiler cannot emit static resumable code, such as F# Interactive (FSI) or top-level functions. Key changes: - TaskSeqDynamicInfo<'T>: concrete ResumptionDynamicInfo subclass that handles MoveNext transitions (same logic as the static MoveNextMethodImpl) - TaskSeqDynamic<'T>: reference-type IAsyncEnumerable implementation using the dynamic path, with proper cloning support for re-enumeration - TaskSeqBuilder.Run: else-branch now creates TaskSeqDynamic instead of raising - taskSeqDynamic: new CE builder (inherits TaskSeqBuilder) and module value; identical to taskSeq in compiled code, uses dynamic path in FSI - 24 new tests covering: empty, single/multi yield, for/while loops, async bind, Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ec731b3 commit c603878

File tree

5 files changed

+578
-10
lines changed

5 files changed

+578
-10
lines changed

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Release notes:
33

44
1.0.0
5+
- adds taskSeqDynamic computation expression and TaskSeqDynamic/TaskSeqDynamicInfo types for dynamic (FSI-compatible) resumable code, fixing issue where taskSeq would raise NotImplementedException in F# Interactive, #246
56
- perf: TaskSeq.chunkBy and chunkByAsync reuse the ResizeArray buffer between chunks, reducing allocations on sequences with many chunk boundaries
67
- fixes: TaskSeq.insertAt, insertManyAt, removeAt, removeManyAt, updateAt now raise ArgumentNullException (not NullReferenceException) when given a null source; insertManyAt also validates the values argument
78
- refactor: simplify lengthBy and lengthBeforeMax to use while! and remove the redundant mutable 'go' and initial MoveNextAsync

src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
<Compile Include="TaskSeq.FirstLastDefault.Tests.fs" />
6969
<Compile Include="TaskSeq.ThreadState.Tests.fs" />
7070
<Compile Include="TaskSeq.Tests.CE.fs" />
71+
<Compile Include="TaskSeq.Dynamic.Tests.CE.fs" />
7172
<Compile Include="TaskSeq.StateTransitionBug.Tests.CE.fs" />
7273
<Compile Include="TaskSeq.StateTransitionBug-delayed.Tests.CE.fs" />
7374
<Compile Include="TaskSeq.Realworld.fs" />
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
module TaskSeq.Tests.``taskSeqDynamic Computation Expression``
2+
3+
open System
4+
open System.Threading
5+
6+
open Xunit
7+
open FsUnit.Xunit
8+
9+
open FSharp.Control
10+
11+
// -------------------------------------------------------
12+
// Basic sanity tests for the taskSeqDynamic CE builder.
13+
// taskSeqDynamic uses the same static path as taskSeq when
14+
// compiled normally, but falls back to the dynamic
15+
// (ResumptionDynamicInfo-based) path in FSI / when the
16+
// F# compiler cannot emit static resumable code.
17+
// -------------------------------------------------------
18+
19+
[<Fact>]
20+
let ``CE taskSeqDynamic empty sequence`` () = task {
21+
let ts = taskSeqDynamic { () }
22+
let! data = ts |> TaskSeq.toListAsync
23+
data |> should be Empty
24+
}
25+
26+
[<Fact>]
27+
let ``CE taskSeqDynamic single yield`` () = task {
28+
let ts = taskSeqDynamic { yield 42 }
29+
let! data = ts |> TaskSeq.toListAsync
30+
data |> should equal [ 42 ]
31+
}
32+
33+
[<Fact>]
34+
let ``CE taskSeqDynamic multiple yields`` () = task {
35+
let ts = taskSeqDynamic {
36+
yield 1
37+
yield 2
38+
yield 3
39+
}
40+
41+
let! data = ts |> TaskSeq.toListAsync
42+
data |> should equal [ 1; 2; 3 ]
43+
}
44+
45+
[<Fact>]
46+
let ``CE taskSeqDynamic yield with for loop`` () = task {
47+
let ts = taskSeqDynamic {
48+
for i in 1..5 do
49+
yield i
50+
}
51+
52+
let! data = ts |> TaskSeq.toListAsync
53+
data |> should equal [ 1; 2; 3; 4; 5 ]
54+
}
55+
56+
[<Fact>]
57+
let ``CE taskSeqDynamic yield with async task bind`` () = task {
58+
let ts = taskSeqDynamic {
59+
let! x = task { return 10 }
60+
yield x
61+
let! y = task { return 20 }
62+
yield y
63+
}
64+
65+
let! data = ts |> TaskSeq.toListAsync
66+
data |> should equal [ 10; 20 ]
67+
}
68+
69+
[<Fact>]
70+
let ``CE taskSeqDynamic yield from seq`` () = task {
71+
let ts = taskSeqDynamic { yield! [ 1; 2; 3 ] }
72+
let! data = ts |> TaskSeq.toListAsync
73+
data |> should equal [ 1; 2; 3 ]
74+
}
75+
76+
[<Fact>]
77+
let ``CE taskSeqDynamic yield from another taskSeqDynamic`` () = task {
78+
let inner = taskSeqDynamic {
79+
yield 1
80+
yield 2
81+
}
82+
83+
let ts = taskSeqDynamic {
84+
yield! inner
85+
yield 3
86+
}
87+
88+
let! data = ts |> TaskSeq.toListAsync
89+
data |> should equal [ 1; 2; 3 ]
90+
}
91+
92+
[<Fact>]
93+
let ``CE taskSeqDynamic yield from taskSeq`` () = task {
94+
let inner = taskSeq {
95+
yield 1
96+
yield 2
97+
}
98+
99+
let ts = taskSeqDynamic { yield! inner }
100+
let! data = ts |> TaskSeq.toListAsync
101+
data |> should equal [ 1; 2 ]
102+
}
103+
104+
[<Fact>]
105+
let ``CE taskSeqDynamic with tryWith`` () = task {
106+
let ts = taskSeqDynamic {
107+
try
108+
yield 1
109+
yield 2
110+
with _ ->
111+
yield -1
112+
}
113+
114+
let! data = ts |> TaskSeq.toListAsync
115+
data |> should equal [ 1; 2 ]
116+
}
117+
118+
[<Fact>]
119+
let ``CE taskSeqDynamic with tryWith catching exception`` () = task {
120+
let ts = taskSeqDynamic {
121+
try
122+
yield 1
123+
raise (InvalidOperationException "test")
124+
yield 2
125+
with :? InvalidOperationException ->
126+
yield 99
127+
}
128+
129+
let! data = ts |> TaskSeq.toListAsync
130+
data |> should equal [ 1; 99 ]
131+
}
132+
133+
[<Fact>]
134+
let ``CE taskSeqDynamic with tryFinally`` () = task {
135+
let mutable finallyCalled = false
136+
137+
let ts = taskSeqDynamic {
138+
try
139+
yield 1
140+
yield 2
141+
finally
142+
finallyCalled <- true
143+
}
144+
145+
let! data = ts |> TaskSeq.toListAsync
146+
data |> should equal [ 1; 2 ]
147+
finallyCalled |> should equal true
148+
}
149+
150+
[<Fact>]
151+
let ``CE taskSeqDynamic with use`` () = task {
152+
let mutable disposed = false
153+
154+
let mkDisposable () =
155+
{ new IDisposable with
156+
member _.Dispose() = disposed <- true
157+
}
158+
159+
let ts = taskSeqDynamic {
160+
use _d = mkDisposable ()
161+
yield 42
162+
}
163+
164+
let! data = ts |> TaskSeq.toListAsync
165+
data |> should equal [ 42 ]
166+
disposed |> should equal true
167+
}
168+
169+
[<Fact>]
170+
let ``CE taskSeqDynamic supports re-enumeration`` () = task {
171+
let ts = taskSeqDynamic {
172+
yield 1
173+
yield 2
174+
yield 3
175+
}
176+
177+
let! data1 = ts |> TaskSeq.toListAsync
178+
let! data2 = ts |> TaskSeq.toListAsync
179+
data1 |> should equal [ 1; 2; 3 ]
180+
data2 |> should equal [ 1; 2; 3 ]
181+
}
182+
183+
[<Fact>]
184+
let ``CE taskSeqDynamic multiple re-enumerations produce same result`` () = task {
185+
let ts = taskSeqDynamic {
186+
for i in 1..10 do
187+
yield i
188+
}
189+
190+
for _ in 1..5 do
191+
let! data = ts |> TaskSeq.toListAsync
192+
data |> should equal [ 1..10 ]
193+
}
194+
195+
[<Fact>]
196+
let ``CE taskSeqDynamic with cancellation`` () = task {
197+
use cts = new CancellationTokenSource()
198+
cts.Cancel()
199+
200+
let ts = taskSeqDynamic {
201+
yield 1
202+
yield 2
203+
yield 3
204+
}
205+
206+
let enumerator = ts.GetAsyncEnumerator(cts.Token)
207+
208+
let mutable threw = false
209+
210+
try
211+
let! _ = enumerator.MoveNextAsync()
212+
()
213+
with :? OperationCanceledException ->
214+
threw <- true
215+
216+
threw |> should equal true
217+
do! enumerator.DisposeAsync()
218+
}
219+
220+
[<Fact>]
221+
let ``CE taskSeqDynamic with large for loop`` () = task {
222+
let ts = taskSeqDynamic {
223+
for i in 1..1000 do
224+
yield i
225+
}
226+
227+
let! data = ts |> TaskSeq.toListAsync
228+
data |> should equal [ 1..1000 ]
229+
data |> should haveLength 1000
230+
}
231+
232+
[<Fact>]
233+
let ``CE taskSeqDynamic with nested for loops`` () = task {
234+
let ts = taskSeqDynamic {
235+
for i in 1..3 do
236+
for j in 1..3 do
237+
yield i * 10 + j
238+
}
239+
240+
let expected = [
241+
for i in 1..3 do
242+
for j in 1..3 do
243+
yield i * 10 + j
244+
]
245+
246+
let! data = ts |> TaskSeq.toListAsync
247+
data |> should equal expected
248+
}
249+
250+
[<Fact>]
251+
let ``CE taskSeqDynamic with async value task bind`` () = task {
252+
let ts = taskSeqDynamic {
253+
let! x = System.Threading.Tasks.ValueTask.FromResult(7)
254+
yield x
255+
}
256+
257+
let! data = ts |> TaskSeq.toListAsync
258+
data |> should equal [ 7 ]
259+
}
260+
261+
[<Fact>]
262+
let ``CE taskSeqDynamic with Async bind`` () = task {
263+
let ts = taskSeqDynamic {
264+
let! x = async { return 99 }
265+
yield x
266+
}
267+
268+
let! data = ts |> TaskSeq.toListAsync
269+
data |> should equal [ 99 ]
270+
}
271+
272+
[<Fact>]
273+
let ``CE taskSeqDynamic is IAsyncEnumerable`` () = task {
274+
// In compiled mode this uses the static path (returns TaskSeq<_,_>),
275+
// in FSI it returns TaskSeqDynamic<_>. Both implement IAsyncEnumerable<int>.
276+
let ts = taskSeqDynamic { yield 1 }
277+
let! data = ts |> TaskSeq.toListAsync
278+
data |> should equal [ 1 ]
279+
}
280+
281+
[<Fact>]
282+
let ``CE taskSeqDynamic empty produces no values`` () = task {
283+
let mutable count = 0
284+
285+
let ts = taskSeqDynamic { () }
286+
287+
let e = ts.GetAsyncEnumerator(CancellationToken.None)
288+
289+
try
290+
while! e.MoveNextAsync() do
291+
count <- count + 1
292+
finally
293+
()
294+
295+
do! e.DisposeAsync()
296+
count |> should equal 0
297+
}
298+
299+
[<Fact>]
300+
let ``CE taskSeqDynamic with while loop`` () = task {
301+
let ts = taskSeqDynamic {
302+
let mutable i = 0
303+
304+
while i < 5 do
305+
yield i
306+
i <- i + 1
307+
}
308+
309+
let! data = ts |> TaskSeq.toListAsync
310+
data |> should equal [ 0; 1; 2; 3; 4 ]
311+
}
312+
313+
[<Fact>]
314+
let ``CE taskSeqDynamic is same type as TaskSeq`` () =
315+
let ts: TaskSeq<int> = taskSeqDynamic { yield 1 }
316+
ts |> should not' (be Null)
317+
318+
[<Fact>]
319+
let ``CE taskSeqDynamic with several yield!`` () = task {
320+
let tskSeq = taskSeqDynamic {
321+
yield! Gen.sideEffectTaskSeq 10
322+
yield! Gen.sideEffectTaskSeq 5
323+
}
324+
325+
let! data = tskSeq |> TaskSeq.toListAsync
326+
data |> should equal (List.concat [ [ 1..10 ]; [ 1..5 ] ])
327+
}

0 commit comments

Comments
 (0)