From f31babd41933b026c1f9254ce7bfb571aa87c487 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 13:22:45 +0000 Subject: [PATCH 1/2] fix: CircularBuffer.GetEnumerator infinite-recursion and destructive enumeration The IEnumerable<'T> and IEnumerable interface implementations each called themselves via (this :> Interface).GetEnumerator(), causing a stack overflow whenever the buffer was enumerated (e.g. Seq.toList, for-in loops). Additionally, the old member GetEnumerator() was destructive: it consumed elements via Dequeue, so iterating the buffer would empty it and could loop forever on an empty buffer. Fix: replace with a non-destructive index-based implementation that reads directly from the internal buffer array using (tail + i) % bufferSize. Also adds 6 new tests covering: - Seq.toList is non-destructive - for-in loop enumeration - Wrapped-around buffer enumeration order - Empty buffer enumeration - IReadOnlyCollection.Count via interface - Enqueue(array, offset) 2-arg overload Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/FSharpx.Collections/CircularBuffer.fs | 20 +++---- .../CircularBufferTests.fs | 52 ++++++++++++++++++- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/FSharpx.Collections/CircularBuffer.fs b/src/FSharpx.Collections/CircularBuffer.fs index 5359b20b..233512da 100644 --- a/src/FSharpx.Collections/CircularBuffer.fs +++ b/src/FSharpx.Collections/CircularBuffer.fs @@ -97,24 +97,16 @@ type CircularBuffer<'T>(bufferSize: int) = member this.Enqueue(value) = this.Enqueue([| value |], 0, 1) - member this.GetEnumerator() = - let rec loop() = - seq { - if length > 0 then - yield this.Dequeue(1).[0] - - yield! loop() - } - - loop().GetEnumerator() + member this.GetEnumerator() : IEnumerator<'T> = + (seq { for i in 0 .. length - 1 -> buffer.[(tail + i) % bufferSize] }).GetEnumerator() interface IEnumerable<'T> with - member this.GetEnumerator() = - (this :> IEnumerable<'T>).GetEnumerator() + member this.GetEnumerator() : IEnumerator<'T> = + this.GetEnumerator() interface IEnumerable with - member this.GetEnumerator() = - (this :> IEnumerable).GetEnumerator() + member this.GetEnumerator() : IEnumerator = + (this.GetEnumerator() :> IEnumerator) interface IReadOnlyCollection<'T> with member this.Count = this.Count diff --git a/tests/FSharpx.Collections.Tests/CircularBufferTests.fs b/tests/FSharpx.Collections.Tests/CircularBufferTests.fs index d371ddc7..10488e06 100644 --- a/tests/FSharpx.Collections.Tests/CircularBufferTests.fs +++ b/tests/FSharpx.Collections.Tests/CircularBufferTests.fs @@ -320,4 +320,54 @@ module CircularBufferTests = //|> Async.Start //// [/snippet] //printfn "CircularStream.AsyncWrite(array) tests passed in %d ms" stopwatch.ElapsedMilliseconds - ] + + test "Enqueue(array, offset) 2-arg overload enqueues from offset to end" { + let circularBuffer = CircularBuffer 5 + circularBuffer.Enqueue([| 10; 20; 30; 40; 50 |], 2) + Expect.equal "count" 3 <| circularBuffer.Count + Expect.equal "dequeued" [| 30; 40; 50 |] <| circularBuffer.Dequeue 3 + } + + test "Seq.toList is non-destructive: buffer is unchanged after enumeration" { + let circularBuffer = CircularBuffer 5 + circularBuffer.Enqueue(1) + circularBuffer.Enqueue(2) + circularBuffer.Enqueue(3) + let asList = Seq.toList circularBuffer + Expect.equal "seq contents" [ 1; 2; 3 ] asList + Expect.equal "count unchanged" 3 <| circularBuffer.Count + } + + test "for-in loop enumerates elements in FIFO order" { + let circularBuffer = CircularBuffer 5 + circularBuffer.Enqueue [| 10; 20; 30 |] + let mutable result = [] + + for x in circularBuffer do + result <- result @ [ x ] + + Expect.equal "for-in" [ 10; 20; 30 ] result + } + + test "Seq.toList on wrapped-around buffer preserves FIFO order" { + let circularBuffer = CircularBuffer 4 + circularBuffer.Enqueue [| 1; 2; 3; 4 |] + circularBuffer.Dequeue 2 |> ignore + circularBuffer.Enqueue [| 5; 6 |] + // Internal state: head has wrapped; FIFO order should be [3;4;5;6] + Expect.equal "wrapped seq" [ 3; 4; 5; 6 ] (Seq.toList circularBuffer) + } + + test "Seq.toList on empty buffer returns empty list" { + let circularBuffer = CircularBuffer 5 + Expect.equal "empty seq" [] (Seq.toList circularBuffer) + } + + test "IReadOnlyCollection Count matches after wrap" { + let circularBuffer = CircularBuffer 3 + circularBuffer.Enqueue [| 1; 2; 3 |] + circularBuffer.Dequeue 1 |> ignore + circularBuffer.Enqueue(4) + let irc = circularBuffer :> System.Collections.Generic.IReadOnlyCollection + Expect.equal "IReadOnlyCollection.Count" 3 irc.Count + } ] From 5c63c23f75527e89159d3236cf3c36321a63141e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 25 Apr 2026 13:22:48 +0000 Subject: [PATCH 2/2] ci: trigger checks