Skip to content

Commit afe8ac4

Browse files
authored
Merge pull request #27 from Innmind/improve-deferred-sequence
Improve deferred sequence
2 parents 5182a4b + 6b059b3 commit afe8ac4

11 files changed

+303
-107
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
### Changed
1010

1111
- Use `static` closures as much as possible to reduce the probability of creating circular references by capturing `$this` as it can lead to memory root buffer exhaustion.
12+
- Remove keeping intermediary values of a deferred `Sequence` that is referenced by no one.
1213

1314
### Fixed
1415

proofs/sequence.php

+73
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,77 @@ static function($assert, $string, $chunk) {
9797
);
9898
},
9999
);
100+
101+
yield proof(
102+
'Sequende::defer() holds intermediary values even when no longer used',
103+
given(
104+
Set\Sequence::of(Set\Type::any()),
105+
Set\Sequence::of(Set\Type::any()),
106+
),
107+
static function($assert, $prefix, $suffix) {
108+
$initial = Sequence::defer((static function() use ($prefix, $suffix) {
109+
foreach ($prefix as $value) {
110+
yield $value;
111+
}
112+
113+
foreach ($suffix as $value) {
114+
yield $value;
115+
}
116+
})());
117+
118+
// This does a partial read on the generator
119+
$assert->same(
120+
$prefix,
121+
$initial
122+
->take(\count($prefix))
123+
->toList(),
124+
);
125+
126+
// The maps are only here to wrap the generator, it doesn't change
127+
// the values
128+
$another = $initial
129+
->map(static fn($value) => [$value])
130+
->map(static fn($values) => $values[0]);
131+
unset($initial);
132+
133+
// If it didn't store the intermediary values the array would miss
134+
// the prefix values due to the partial read on the initial
135+
// generator due to the ->take()->toList() call above
136+
$assert->same(
137+
[...$prefix, ...$suffix],
138+
$another->toList(),
139+
);
140+
},
141+
);
142+
143+
yield proof(
144+
"Sequence::defer() stack trace doesn't show intermediary sequences when not used",
145+
given(Set\Integers::between(1, 10)),
146+
static function($assert, $calls) {
147+
$expected = null;
148+
$sequence = Sequence::defer((static function() use (&$expected) {
149+
yield null;
150+
151+
throw $expected = new Exception;
152+
})());
153+
154+
for ($i = 0; $i < $calls; $i++) {
155+
$sequence = $sequence->map(static fn($value) => $value);
156+
}
157+
158+
try {
159+
$sequence->toList();
160+
$assert->fail('it should throw');
161+
} catch (Exception $e) {
162+
$assert->same($expected, $e);
163+
164+
$accumulations = \array_filter(
165+
$e->getTrace(),
166+
static fn($frame) => \str_ends_with($frame['file'] ?? '', 'src/Accumulate.php'),
167+
);
168+
169+
$assert->count(1, $accumulations);
170+
}
171+
},
172+
);
100173
};

src/Accumulate.php

+20-16
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,21 @@
77
* Simple iterator to cache the results of a generator so it can be iterated
88
* over multiple times
99
*
10-
* @template T
1110
* @template S
12-
* @implements \Iterator<T, S>
11+
* @implements \Iterator<S>
1312
* @internal Do not use this in your code
1413
* @psalm-immutable Not really immutable but to simplify declaring immutability of other structures
1514
*/
1615
final class Accumulate implements \Iterator
1716
{
18-
/** @var \Generator<T, S> */
17+
/** @var \Generator<S> */
1918
private \Generator $generator;
20-
/** @var list<T> */
21-
private array $keys = [];
2219
/** @var list<S> */
2320
private array $values = [];
21+
private bool $started = false;
2422

2523
/**
26-
* @param \Generator<T, S> $generator
24+
* @param \Generator<S> $generator
2725
*/
2826
public function __construct(\Generator $generator)
2927
{
@@ -35,27 +33,31 @@ public function __construct(\Generator $generator)
3533
*/
3634
public function current(): mixed
3735
{
36+
/** @psalm-suppress InaccessibleProperty */
37+
$this->started = true;
3838
/** @psalm-suppress UnusedMethodCall */
3939
$this->pop();
4040

4141
return \current($this->values);
4242
}
4343

4444
/**
45-
* @return T
45+
* @return int<0, max>|null
4646
*/
47-
public function key(): mixed
47+
public function key(): ?int
4848
{
49+
/** @psalm-suppress InaccessibleProperty */
50+
$this->started = true;
4951
/** @psalm-suppress UnusedMethodCall */
5052
$this->pop();
5153

52-
return \current($this->keys);
54+
return \key($this->values);
5355
}
5456

5557
public function next(): void
5658
{
5759
/** @psalm-suppress InaccessibleProperty */
58-
\next($this->keys);
60+
$this->started = true;
5961
/** @psalm-suppress InaccessibleProperty */
6062
\next($this->values);
6163

@@ -68,13 +70,15 @@ public function next(): void
6870
public function rewind(): void
6971
{
7072
/** @psalm-suppress InaccessibleProperty */
71-
\reset($this->keys);
73+
$this->started = true;
7274
/** @psalm-suppress InaccessibleProperty */
7375
\reset($this->values);
7476
}
7577

7678
public function valid(): bool
7779
{
80+
/** @psalm-suppress InaccessibleProperty */
81+
$this->started = true;
7882
/** @psalm-suppress ImpureMethodCall */
7983
$valid = !$this->reachedCacheEnd() || $this->generator->valid();
8084

@@ -88,6 +92,11 @@ public function valid(): bool
8892
return $valid;
8993
}
9094

95+
public function started(): bool
96+
{
97+
return $this->started;
98+
}
99+
91100
private function reachedCacheEnd(): bool
92101
{
93102
return \key($this->values) === null;
@@ -96,11 +105,6 @@ private function reachedCacheEnd(): bool
96105
private function pop(): void
97106
{
98107
if ($this->reachedCacheEnd()) {
99-
/**
100-
* @psalm-suppress InaccessibleProperty
101-
* @psalm-suppress ImpureMethodCall
102-
*/
103-
$this->keys[] = $this->generator->key();
104108
/**
105109
* @psalm-suppress InaccessibleProperty
106110
* @psalm-suppress ImpureMethodCall

0 commit comments

Comments
 (0)