Skip to content

Commit 1d4f250

Browse files
authored
Merge pull request #2680 from mpilquist/bugfix/2679
Fix #2679 - bug in Chunk.compactUntagged when used with empty or singleton chunks
2 parents c945743 + c551fd3 commit 1d4f250

File tree

2 files changed

+30
-8
lines changed

2 files changed

+30
-8
lines changed

core/shared/src/main/scala/fs2/Chunk.scala

+26-8
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,18 @@ abstract class Chunk[+O] extends Serializable with ChunkPlatform[O] with ChunkRu
8888
/** Copies the elements of this chunk in to the specified array at the specified start index. */
8989
def copyToArray[O2 >: O](xs: Array[O2], start: Int = 0): Unit
9090

91-
/** Converts this chunk to a chunk backed by a single array. */
91+
/** Converts this chunk to a chunk backed by a single array.
92+
*
93+
* Alternatively, call `toIndexedChunk` to get back a chunk with guaranteed O(1) indexed lookup
94+
* while also minimizing copying.
95+
*/
9296
def compact[O2 >: O](implicit ct: ClassTag[O2]): Chunk.ArraySlice[O2] =
9397
Chunk.ArraySlice(toArray[O2], 0, size)
9498

9599
/** Like `compact` but does not require a `ClassTag`. Elements are boxed and stored in an `Array[Any]`. */
96-
def compactUntagged[O2 >: O]: Chunk.ArraySlice[O2] = {
97-
val arr = new Array[Any](size)
98-
copyToArray(arr, 0)
99-
Chunk.array(arr).asInstanceOf[Chunk.ArraySlice[O2]]
100-
}
100+
@deprecated("Unsound when used with primitives, use compactBoxed instead", "3.1.6")
101+
def compactUntagged[O2 >: O]: Chunk.ArraySlice[O2] =
102+
Chunk.ArraySlice(toArray[Any], 0, size).asInstanceOf[Chunk.ArraySlice[O2]]
101103

102104
/** Drops the first `n` elements of this chunk. */
103105
def drop(n: Int): Chunk[O] = splitAt(n)._2
@@ -311,6 +313,21 @@ abstract class Chunk[+O] extends Serializable with ChunkPlatform[O] with ChunkRu
311313
if (isEmpty) Chain.empty
312314
else Chain.fromSeq(toList)
313315

316+
/** Returns a chunk with guaranteed O(1) lookup by index.
317+
*
318+
* Unlike `compact`, this operation does not copy any elements unless this chunk
319+
* does not provide O(1) lookup by index -- e.g., a chunk built via 1 or more usages
320+
* of `++`.
321+
*/
322+
def toIndexedChunk: Chunk[O] = this match {
323+
case _: Chunk.Queue[_] =>
324+
val b = collection.mutable.Buffer.newBuilder[O]
325+
b.sizeHint(size)
326+
foreach(o => b += o)
327+
Chunk.buffer(b.result())
328+
case other => other
329+
}
330+
314331
/** Converts this chunk to a list. */
315332
def toList: List[O] =
316333
if (isEmpty) Nil
@@ -678,6 +695,7 @@ object Chunk
678695
this.asInstanceOf[ArraySlice[O2]]
679696
else super.compact
680697

698+
@deprecated("Unsound", "3.1.6")
681699
override def compactUntagged[O2 >: O]: ArraySlice[O2] =
682700
if ((classOf[Array[Any]] eq values.getClass) && offset == 0 && length == values.length)
683701
this.asInstanceOf[ArraySlice[O2]]
@@ -1066,12 +1084,12 @@ object Chunk
10661084
}
10671085

10681086
override def traverse[F[_], O2](f: O => F[O2])(implicit F: Applicative[F]): F[Chunk[O2]] =
1069-
compactUntagged.traverse(f)
1087+
toIndexedChunk.traverse(f)
10701088

10711089
override def traverseFilter[F[_], O2](f: O => F[Option[O2]])(implicit
10721090
F: Applicative[F]
10731091
): F[Chunk[O2]] =
1074-
compactUntagged.traverseFilter(f)
1092+
toIndexedChunk.traverseFilter(f)
10751093
}
10761094

10771095
object Queue {

core/shared/src/test/scala/fs2/ChunkSuite.scala

+4
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,8 @@ class ChunkSuite extends Fs2Suite {
211211
Chunk.ArraySlice(Array[Any](0)).asInstanceOf[Chunk[Int]].toArray[Any]
212212
Chunk.ArraySlice(Array[Any](0)).asInstanceOf[Chunk[Int]].toArray[Int]
213213
}
214+
215+
test("compactUntagged - regression #2679") {
216+
Chunk.Queue.singleton(Chunk.singleton(1)).traverse(x => Option(x))
217+
}
214218
}

0 commit comments

Comments
 (0)