Skip to content

Commit 223e207

Browse files
committed
Merge pull request scala#3848 from Ichoran/issue/8680
SI-8680 Stream.addString is too eager
2 parents 75f1235 + 039f3e3 commit 223e207

File tree

4 files changed

+198
-14
lines changed

4 files changed

+198
-14
lines changed

bincompat-backward.whitelist.conf

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,19 @@ filter {
194194
{
195195
matchName="scala.collection.immutable.Stream.filteredTail"
196196
problemName=MissingMethodProblem
197+
},
198+
// https://github.com/scala/scala/pull/3848 -- SI-8680
199+
{
200+
matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$6"
201+
problemName=MissingMethodProblem
202+
},
203+
{
204+
matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$5"
205+
problemName=MissingMethodProblem
206+
},
207+
{
208+
matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$4"
209+
problemName=MissingMethodProblem
197210
}
198211
]
199212
}

bincompat-forward.whitelist.conf

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,27 @@ filter {
368368
{
369369
matchName="scala.reflect.io.AbstractFile.filterImpl"
370370
problemName=MissingMethodProblem
371+
},
372+
// https://github.com/scala/scala/pull/3848 -- SI-8680
373+
{
374+
matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$6"
375+
problemName=MissingMethodProblem
376+
},
377+
{
378+
matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$5"
379+
problemName=MissingMethodProblem
380+
},
381+
{
382+
matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$4"
383+
problemName=MissingMethodProblem
384+
},
385+
{
386+
matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$3"
387+
problemName=MissingMethodProblem
388+
},
389+
{
390+
matchName="scala.collection.immutable.Stream.scala$collection$immutable$Stream$$loop$2"
391+
problemName=MissingMethodProblem
371392
}
372393
]
373394
}

src/library/scala/collection/immutable/Stream.scala

Lines changed: 111 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,12 @@ import scala.language.implicitConversions
176176
* loop(1, 1)
177177
* }
178178
* }}}
179+
*
180+
* Note that `mkString` forces evaluation of a `Stream`, but `addString` does
181+
* not. In both cases, a `Stream` that is or ends in a cycle
182+
* (e.g. `lazy val s: Stream[Int] = 0 #:: s`) will convert additional trips
183+
* through the cycle to `...`. Additionally, `addString` will display an
184+
* un-memoized tail as `?`.
179185
*
180186
* @tparam A the type of the elements contained in this stream.
181187
*
@@ -253,12 +259,22 @@ self =>
253259
* @note Often we use `Stream`s to represent an infinite set or series. If
254260
* that's the case for your particular `Stream` then this function will never
255261
* return and will probably crash the VM with an `OutOfMemory` exception.
262+
* This function will not hang on a finite cycle, however.
256263
*
257264
* @return The fully realized `Stream`.
258265
*/
259266
def force: Stream[A] = {
260-
var these = this
261-
while (!these.isEmpty) these = these.tail
267+
// Use standard 2x 1x iterator trick for cycle detection ("those" is slow one)
268+
var these, those = this
269+
if (!these.isEmpty) these = these.tail
270+
while (those ne these) {
271+
if (these.isEmpty) return this
272+
these = these.tail
273+
if (these.isEmpty) return this
274+
these = these.tail
275+
if (these eq those) return this
276+
those = those.tail
277+
}
262278
this
263279
}
264280

@@ -309,9 +325,24 @@ self =>
309325

310326
override def toStream: Stream[A] = this
311327

312-
override def hasDefiniteSize = {
313-
def loop(s: Stream[A]): Boolean = s.isEmpty || s.tailDefined && loop(s.tail)
314-
loop(this)
328+
override def hasDefiniteSize: Boolean = isEmpty || {
329+
if (!tailDefined) false
330+
else {
331+
// Two-iterator trick (2x & 1x speed) for cycle detection.
332+
var those = this
333+
var these = tail
334+
while (those ne these) {
335+
if (these.isEmpty) return true
336+
if (!these.tailDefined) return false
337+
these = these.tail
338+
if (these.isEmpty) return true
339+
if (!these.tailDefined) return false
340+
these = these.tail
341+
if (those eq these) return false
342+
those = those.tail
343+
}
344+
false // Cycle detected
345+
}
315346
}
316347

317348
/** Create a new stream which contains all elements of this stream followed by
@@ -690,7 +721,8 @@ self =>
690721
* `end`. Inside, the string representations of defined elements (w.r.t.
691722
* the method `toString()`) are separated by the string `sep`. The method will
692723
* not force evaluation of undefined elements. A tail of such elements will be
693-
* represented by a `"?"` instead.
724+
* represented by a `"?"` instead. A cyclic stream is represented by a `"..."`
725+
* at the point where the cycle repeats.
694726
*
695727
* @param b The [[collection.mutable.StringBuilder]] factory to which we need
696728
* to add the string elements.
@@ -701,16 +733,81 @@ self =>
701733
* resulting string.
702734
*/
703735
override def addString(b: StringBuilder, start: String, sep: String, end: String): StringBuilder = {
704-
def loop(pre: String, these: Stream[A]) {
705-
if (these.isEmpty) b append end
706-
else {
707-
b append pre append these.head
708-
if (these.tailDefined) loop(sep, these.tail)
709-
else b append sep append "?" append end
736+
b append start
737+
if (!isEmpty) {
738+
b append head
739+
var cursor = this
740+
var n = 1
741+
if (cursor.tailDefined) { // If tailDefined, also !isEmpty
742+
var scout = tail
743+
if (scout.isEmpty) {
744+
// Single element. Bail out early.
745+
b append end
746+
return b
747+
}
748+
if ((cursor ne scout) && scout.tailDefined) {
749+
cursor = scout
750+
scout = scout.tail
751+
// Use 2x 1x iterator trick for cycle detection; slow iterator can add strings
752+
while ((cursor ne scout) && scout.tailDefined) {
753+
b append sep append cursor.head
754+
n += 1
755+
cursor = cursor.tail
756+
scout = scout.tail
757+
if (scout.tailDefined) scout = scout.tail
758+
}
759+
}
760+
if (!scout.tailDefined) { // Not a cycle, scout hit an end
761+
while (cursor ne scout) {
762+
b append sep append cursor.head
763+
n += 1
764+
cursor = cursor.tail
765+
}
766+
}
767+
else {
768+
// Cycle.
769+
// If we have a prefix of length P followed by a cycle of length C,
770+
// the scout will be at position (P%C) in the cycle when the cursor
771+
// enters it at P. They'll then collide when the scout advances another
772+
// C - (P%C) ahead of the cursor.
773+
// If we run the scout P farther, then it will be at the start of
774+
// the cycle: (C - (P%C) + (P%C)) == C == 0. So if another runner
775+
// starts at the beginning of the prefix, they'll collide exactly at
776+
// the start of the loop.
777+
var runner = this
778+
var k = 0
779+
while (runner ne scout) {
780+
runner = runner.tail
781+
scout = scout.tail
782+
k += 1
783+
}
784+
// Now runner and scout are at the beginning of the cycle. Advance
785+
// cursor, adding to string, until it hits; then we'll have covered
786+
// everything once. If cursor is already at beginning, we'd better
787+
// advance one first unless runner didn't go anywhere (in which case
788+
// we've already looped once).
789+
if ((cursor eq scout) && (k > 0)) {
790+
b append sep append cursor.head
791+
n += 1
792+
cursor = cursor.tail
793+
}
794+
while (cursor ne scout) {
795+
b append sep append cursor.head
796+
n += 1
797+
cursor = cursor.tail
798+
}
799+
// Subtract prefix length from total length for cycle reporting.
800+
// (Not currently used, but probably a good idea for the future.)
801+
n -= k
802+
}
803+
}
804+
if (!cursor.isEmpty) {
805+
// Either undefined or cyclic; we can check with tailDefined
806+
if (!cursor.tailDefined) b append sep append "?"
807+
else b append sep append "..."
710808
}
711809
}
712-
b append start
713-
loop("", this)
810+
b append end
714811
b
715812
}
716813

test/files/run/t8680.scala

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
object Test extends App {
2+
def pre(n: Int) = (-n to -1).toStream
3+
4+
def cyc(m: Int) = {
5+
lazy val s: Stream[Int] = (0 until m).toStream #::: s
6+
s
7+
}
8+
9+
def precyc(n: Int, m: Int) = pre(n) #::: cyc(m)
10+
11+
def str(s: Stream[Int]) = {
12+
val b = new StringBuilder
13+
s.addString(b, "", "", "")
14+
b.toString
15+
}
16+
17+
def goal(n: Int, m: Int) = (-n until m).mkString + "..."
18+
19+
// Check un-forced cyclic and non-cyclic streams
20+
assert(str(pre(2)) == pre(2).take(1).toList.mkString + "?")
21+
assert(str(cyc(2)) == cyc(2).take(1).toList.mkString + "?")
22+
assert(str(precyc(2,2)) == precyc(2,2).take(1).toList.mkString + "?")
23+
assert(!pre(2).hasDefiniteSize)
24+
assert(!cyc(2).hasDefiniteSize)
25+
assert(!precyc(2,2).hasDefiniteSize)
26+
27+
// Check forced cyclic and non-cyclic streams
28+
assert(str(pre(2).force) == (-2 to -1).mkString)
29+
assert(str(cyc(2).force) == (0 until 2).mkString + "...")
30+
assert(str(precyc(2,2).force) == (-2 until 2).mkString + "...")
31+
assert(pre(2).force.hasDefiniteSize)
32+
assert(!cyc(2).force.hasDefiniteSize)
33+
assert(!precyc(2,2).force.hasDefiniteSize)
34+
35+
// Special cases
36+
assert(str(cyc(1).force) == goal(0,1))
37+
assert(str(precyc(1,6).force) == goal(1,6))
38+
assert(str(precyc(6,1).force) == goal(6,1))
39+
40+
// Make sure there are no odd/even problems
41+
for (n <- 3 to 4; m <- 3 to 4) {
42+
assert(precyc(n,m).mkString == goal(n,m), s"mkString $n $m")
43+
assert(!precyc(n,m).force.hasDefiniteSize, s"hasDef $n$m")
44+
}
45+
46+
// Make sure there are no cycle/prefix modulus problems
47+
for (i <- 6 to 8) {
48+
assert(precyc(i,3).mkString == goal(i,3), s"mkString $i 3")
49+
assert(precyc(3,i).mkString == goal(3,i), s"mkString 3 $i")
50+
assert(!precyc(i,3).force.hasDefiniteSize, s"hasDef $i 3")
51+
assert(!precyc(3,i).force.hasDefiniteSize, s"hasDef 3 $i")
52+
}
53+
}

0 commit comments

Comments
 (0)