Skip to content

Commit b61024e

Browse files
authored
Fix RecursionError with concat and LazySeqs (#588)
* Fix RecursionError with concat and LazySeqs * Fix RecursionError in concat of LazySeq * LazySeq with_meta fix * Changelog * Fix it
1 parent 5e6b789 commit b61024e

File tree

5 files changed

+84
-49
lines changed

5 files changed

+84
-49
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
* Added `*basilisp-version*` and `*python-version*` Vars to `basilisp.core` (#584)
1313
* Added support for function decorators to `defn` (#585)
1414
* Added the current Python version (`:lpy36`, `:lpy37`, etc.) as a default reader feature for reader conditionals (#585)
15+
* Added `lazy-cat` function for lazily concatenating sequences (#588)
16+
17+
### Changed
18+
* Moved `basilisp.lang.runtime.to_seq` to `basilisp.lang.seq` so it can be used within that module and by `basilisp.lang.runtime` without circular import (#588)
1519

1620
### Fixed
1721
* Fixed a bug where `def` forms did not permit recursive references to the `def`'ed Vars (#578)
22+
* Fixed a bug where `concat` could cause a `RecursionEror` if used on a `LazySeq` instance which itself calls `concat` (#588)
1823

1924
## [v0.1.dev14] - 2020-06-18
2025
### Added

src/basilisp/core.lpy

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2172,6 +2172,12 @@
21722172
(list 'basilisp.lang.seq/LazySeq
21732173
(concat '(fn* []) body)))
21742174

2175+
(defmacro lazy-cat
2176+
"Return a lazy sequence of the concatenation of `colls`. None of the input
2177+
collections will be evaluated until it is needed."
2178+
[& colls]
2179+
`(concat ~@(map (fn [coll] `(lazy-seq ~coll)) colls)))
2180+
21752181
(defn dorun
21762182
"Force a lazy sequence be fully realized. Returns nil.
21772183

src/basilisp/lang/runtime.py

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
IPersistentSet,
5151
IPersistentVector,
5252
ISeq,
53-
ISeqable,
5453
ITransientSet,
5554
)
5655
from basilisp.lang.reference import ReferenceBase
@@ -939,37 +938,12 @@ def cons(o, seq) -> ISeq:
939938
return _cons(seq, o)
940939

941940

942-
def _seq_or_nil(s: ISeq) -> Optional[ISeq]:
943-
"""Return None if a ISeq is empty, the ISeq otherwise."""
944-
if s.is_empty:
945-
return None
946-
return s
947-
948-
949-
@functools.singledispatch
950-
def to_seq(o) -> Optional[ISeq]:
951-
"""Coerce the argument o to a ISeq. If o is None, return None."""
952-
return _seq_or_nil(lseq.sequence(o))
953-
954-
955-
@to_seq.register(type(None))
956-
def _to_seq_none(_) -> None:
957-
return None
958-
959-
960-
@to_seq.register(ISeq)
961-
def _to_seq_iseq(o: ISeq) -> Optional[ISeq]:
962-
return _seq_or_nil(o)
963-
964-
965-
@to_seq.register(ISeqable)
966-
def _to_seq_iseqable(o: ISeqable) -> Optional[ISeq]:
967-
return _seq_or_nil(o.seq())
941+
to_seq = lseq.to_seq
968942

969943

970944
def concat(*seqs) -> ISeq:
971945
"""Concatenate the sequences given by seqs into a single ISeq."""
972-
allseqs = lseq.sequence(itertools.chain(*filter(None, map(to_seq, seqs))))
946+
allseqs = lseq.sequence(itertools.chain.from_iterable(filter(None, seqs)))
973947
if allseqs is None:
974948
return lseq.EMPTY
975949
return allseqs

src/basilisp/lang/seq.py

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
import functools
12
from typing import Any, Callable, Iterable, Iterator, Optional, TypeVar
23

3-
from basilisp.lang.interfaces import IPersistentMap, ISeq, ISequential, IWithMeta
4+
from basilisp.lang.interfaces import (
5+
IPersistentMap,
6+
ISeq,
7+
ISeqable,
8+
ISequential,
9+
IWithMeta,
10+
)
411
from basilisp.util import Maybe
512

613
T = TypeVar("T")
@@ -128,60 +135,57 @@ def cons(self, elem):
128135
return Cons(elem, self)
129136

130137

138+
LazySeqGenerator = Callable[[], Optional[ISeq[T]]]
139+
140+
131141
class LazySeq(IWithMeta, ISequential, ISeq[T]):
132142
"""LazySeqs are wrappers for delaying sequence computation. Create a LazySeq
133143
with a function that can either return None or a Seq. If a Seq is returned,
134144
the LazySeq is a proxy to that Seq."""
135145

136-
__slots__ = ("_gen", "_realized", "_seq", "_meta")
146+
__slots__ = ("_gen", "_seq", "_meta")
137147

138148
# pylint:disable=assigning-non-slot
139149
def __init__(
140150
self,
141-
gen: Callable[[], Optional[ISeq[T]]],
151+
gen: Optional[LazySeqGenerator],
152+
seq: Optional[ISeq[T]] = None,
142153
*,
143154
meta: Optional[IPersistentMap] = None,
144155
) -> None:
145-
self._gen = gen
146-
self._realized = False
147-
self._seq: Optional[ISeq[T]] = None
156+
self._gen: Optional[LazySeqGenerator] = gen
157+
self._seq: Optional[ISeq[T]] = seq
148158
self._meta = meta
149159

150160
@property
151161
def meta(self) -> Optional[IPersistentMap]:
152162
return self._meta
153163

154164
def with_meta(self, meta: Optional[IPersistentMap]) -> "LazySeq[T]":
155-
return LazySeq(self._gen, meta=meta)
165+
return LazySeq(self._gen, seq=self._seq, meta=meta)
156166

157167
# pylint:disable=assigning-non-slot
158168
def _realize(self):
159-
if not self._realized:
160-
self._seq = self._gen()
161-
self._realized = True
169+
if self._gen is not None:
170+
self._seq = to_seq(self._gen())
171+
self._gen = None
162172

163173
@property
164174
def is_empty(self) -> bool:
165-
if not self._realized:
166-
self._realize()
167-
return self.is_empty
168-
if self._seq is None or self._seq.is_empty:
169-
return True
170-
return False
175+
self._realize()
176+
return self._seq is None
171177

172178
@property
173179
def first(self) -> Optional[T]:
174-
if not self._realized:
175-
self._realize()
180+
self._realize()
176181
try:
177182
return self._seq.first # type: ignore
178183
except AttributeError:
179184
return None
180185

181186
@property
182187
def rest(self) -> "ISeq[T]":
183-
if not self._realized:
184-
self._realize()
188+
self._realize()
185189
try:
186190
return self._seq.rest # type: ignore
187191
except AttributeError:
@@ -192,7 +196,7 @@ def cons(self, elem):
192196

193197
@property
194198
def is_realized(self):
195-
return self._realized
199+
return self._gen is None
196200

197201

198202
def sequence(s: Iterable) -> ISeq[Any]:
@@ -202,3 +206,31 @@ def sequence(s: Iterable) -> ISeq[Any]:
202206
return _Sequence(i, next(i))
203207
except StopIteration:
204208
return EMPTY
209+
210+
211+
def _seq_or_nil(s: ISeq) -> Optional[ISeq]:
212+
"""Return None if a ISeq is empty, the ISeq otherwise."""
213+
if s.is_empty:
214+
return None
215+
return s
216+
217+
218+
@functools.singledispatch
219+
def to_seq(o) -> Optional[ISeq]:
220+
"""Coerce the argument o to a ISeq. If o is None, return None."""
221+
return _seq_or_nil(sequence(o))
222+
223+
224+
@to_seq.register(type(None))
225+
def _to_seq_none(_) -> None:
226+
return None
227+
228+
229+
@to_seq.register(ISeq)
230+
def _to_seq_iseq(o: ISeq) -> Optional[ISeq]:
231+
return _seq_or_nil(o)
232+
233+
234+
@to_seq.register(ISeqable)
235+
def _to_seq_iseqable(o: ISeqable) -> Optional[ISeq]:
236+
return _seq_or_nil(o.seq())

tests/basilisp/test_core_macros.lpy

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,25 @@
349349
#{1 3 5} :>> dec
350350
:a)))))
351351

352+
(deftest lazy-cat-test
353+
(is (= '(1 2 3 4 5 6) (lazy-cat [1 2 3] [4 5 6])))
354+
355+
(testing "laziness"
356+
(let [a (atom nil)]
357+
(is (= '(1 2 3 4 5)
358+
(take 5
359+
(lazy-cat [1 2 3 4 5 6]
360+
(map #(reset! a %) [7 8 9 0])))))
361+
(is (nil? @a))
362+
(is (= [1 2 3 4 5 6 7 8 9 0]
363+
(lazy-cat [1 2 3 4 5]
364+
(map #(reset! a %) [6 7 8 9 0]))))
365+
(is (zero? @a)))))
366+
352367
(deftest for-test
368+
(testing "no recursion error"
369+
(is (= 10000 (count (for [x (range 100), y (range 100)] [x y])))))
370+
353371
(testing "basic for comprehensions"
354372
(is (= [1 2 3 4] (for [x [1 2 3 4]] x)))
355373

0 commit comments

Comments
 (0)