Skip to content

Commit

Permalink
Refactor Coroutine to use Future
Browse files Browse the repository at this point in the history
Removed unwrap() from Thenable. Made Promise, Delayed, and Coroutine final.
  • Loading branch information
trowski committed Nov 16, 2015
1 parent cb1bfae commit 789903e
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 117 deletions.
149 changes: 69 additions & 80 deletions src/Coroutine/Coroutine.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
use Exception;
use Generator;
use Icicle\Loop;
use Icicle\Promise\Promise;
use Icicle\Promise\Future;
use Icicle\Promise\Thenable;

/**
* This class implements cooperative coroutines using Generators. Coroutines should yield promises to pause execution
* of the coroutine until the promise has resolved. If the promise is fulfilled, the fulfillment value is sent to the
* generator. If the promise is rejected, the rejection exception is thrown into the generator.
*/
class Coroutine extends Promise
final class Coroutine extends Future
{
/**
* @var \Generator|null
Expand Down Expand Up @@ -52,93 +52,68 @@ class Coroutine extends Promise
*/
public function __construct(Generator $generator)
{
parent::__construct();

$this->generator = $generator;

parent::__construct(
function (callable $resolve, callable $reject) {
$yielded = $this->generator->current();

if (!$this->generator->valid()) {
$resolve();
$this->close();
return;
}

/**
* @param mixed $value The value to send to the generator.
*/
$this->send = function ($value = null) use ($resolve, $reject) {
if ($this->paused) { // If paused, save callable and value for resuming.
$this->next = [$this->send, $value];
return;
}

try {
// Send the new value and execute to next yield statement.
$yielded = $this->generator->send($value);

if (!$this->generator->valid()) {
$resolve($value);
$this->close();
return;
}

$this->next($yielded);
} catch (Exception $exception) {
$reject($exception);
$this->close();
}
};

/**
* @param \Exception $exception Exception to be thrown into the generator.
*/
$this->capture = function (Exception $exception) use ($resolve, $reject) {
if ($this->paused) { // If paused, save callable and exception for resuming.
$this->next = [$this->capture, $exception];
return;
}

try {
// Throw exception at current execution point.
$yielded = $this->generator->throw($exception);

if (!$this->generator->valid()) {
$resolve();
$this->close();
return;
}

$this->next($yielded);
} catch (Exception $exception) {
$reject($exception);
$this->close();
}
};

$this->next($yielded);

return function (Exception $exception) {
try {
$current = $this->generator->current(); // Get last yielded value.
if ($current instanceof Thenable) {
$current->cancel($exception);
}
} finally {
$this->close();
}
};
/**
* @param mixed $value The value to send to the generator.
*/
$this->send = function ($value = null) {
if ($this->paused) { // If paused, save callable and value for resuming.
$this->next = [$this->send, $value];
return;
}

try {
// Send the new value and execute to next yield statement.
$this->next($this->generator->send($value), $value);
} catch (Exception $exception) {
$this->reject($exception);
$this->close();
}
);
};

/**
* @param \Exception $exception Exception to be thrown into the generator.
*/
$this->capture = function (Exception $exception) {
if ($this->paused) { // If paused, save callable and exception for resuming.
$this->next = [$this->capture, $exception];
return;
}

try {
// Throw exception at current execution point.
$this->next($this->generator->throw($exception));
} catch (Exception $exception) {
$this->reject($exception);
$this->close();
}
};

try {
$this->next($this->generator->current());
} catch (Exception $exception) {
$this->reject($exception);
$this->close();
}
}

/**
* Examines the value yielded from the generator and prepares the next step in interation.
*
* @param mixed $yielded
* @param mixed $last
*/
private function next($yielded)
private function next($yielded, $last = null)
{
if (!$this->generator->valid()) {
$this->resolve($last);
$this->close();
return;
}

if ($yielded instanceof Generator) {
$yielded = new self($yielded);
}
Expand All @@ -149,7 +124,7 @@ private function next($yielded)
Loop\queue($this->send, $yielded);
}
}

/**
* The garbage collector does not automatically detect (at least not quickly) the circular references that can be
* created, so explicitly setting these parameters to null is necessary for proper freeing of memory.
Expand Down Expand Up @@ -202,7 +177,21 @@ public function isPaused()
*/
public function cancel($reason = null)
{
$this->pause();
try {
$current = $this->generator->current(); // Get last yielded value.
if ($current instanceof Thenable) {
$current->cancel($reason); // Cancel last yielded awaitable.
}
} catch (Exception $exception) {
$reason = $exception;
}

try {
$this->close(); // Throwing finally blocks in the Generator may cause close() to throw.
} catch (Exception $exception) {
$reason = $exception;
}

parent::cancel($reason);
}
}
2 changes: 1 addition & 1 deletion src/Promise/Delayed.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
/**
* Awaitable implementation that should not be returned from a public API, but used only internally.
*/
class Delayed extends Future
final class Delayed extends Future
{
/**
* {@inheritdoc}
Expand Down
20 changes: 10 additions & 10 deletions src/Promise/Future.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,24 +67,24 @@ public function __construct(callable $onCancelled = null)
*
* @param mixed $value A promise can be resolved with anything other than itself.
*/
protected function resolve($result)
protected function resolve($value = null)
{
if (null !== $this->result) {
return;
}

if ($result instanceof self) {
$result = $result->unwrap();
if ($this === $result) {
$result = new RejectedPromise(
if ($value instanceof self) {
$value = $value->unwrap();
if ($this === $value) {
$value = new RejectedPromise(
new CircularResolutionError('Circular reference in promise resolution chain.')
);
}
} elseif (!$result instanceof Thenable) {
$result = new FulfilledPromise($result);
} elseif (!$value instanceof Thenable) {
$value = new FulfilledPromise($value);
}

$this->result = $result;
$this->result = $value;
$this->result->done($this->onFulfilled, $this->onRejected ?: new ThenQueue());

$this->onFulfilled = null;
Expand Down Expand Up @@ -162,7 +162,7 @@ public function then(callable $onFulfilled = null, callable $onRejected = null)
public function done(callable $onFulfilled = null, callable $onRejected = null)
{
if (null !== $this->result) {
$this->result->done($onFulfilled, $onRejected);
$this->unwrap()->done($onFulfilled, $onRejected);
return;
}

Expand Down Expand Up @@ -224,7 +224,7 @@ public function timeout($timeout, $reason = null)
$this->cancel($reason);
});

$future = new self(function (Exception $exception) use (&$timer) {
$future = new self(function (Exception $exception) use ($timer) {
$timer->stop();

if (0 === --$this->children) {
Expand Down
8 changes: 0 additions & 8 deletions src/Promise/Internal/LazyPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,4 @@ public function wait()
{
return $this->getPromise()->wait();
}

/**
* {@inheritdoc}
*/
public function unwrap()
{
return $this->getPromise()->unwrap();
}
}
8 changes: 0 additions & 8 deletions src/Promise/Internal/ResolvedPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,4 @@ public function delay($time)
{
return $this;
}

/**
* {@inheritdoc}
*/
public function unwrap()
{
return $this;
}
}
2 changes: 1 addition & 1 deletion src/Promise/Promise.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*
* @see http://promisesaplus.com
*/
class Promise extends Future
final class Promise extends Future
{
/**
* @param callable<(callable $resolve, callable $reject, Loop $loop): callable|null> $resolver
Expand Down
9 changes: 0 additions & 9 deletions src/Promise/Thenable.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,4 @@ public function isRejected();
* @return bool
*/
public function isCancelled();

/**
* Iteratively finds the last promise in the pending chain and returns it.
*
* @return \Icicle\Promise\Thenable
*
* @internal Used to keep promise methods from exceeding the call stack depth limit.
*/
//public function unwrap();
}
37 changes: 37 additions & 0 deletions tests/Coroutine/CoroutineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,43 @@ public function testGeneratorThrowingExceptionWithFinallyYieldingPendingPromise(
$this->assertSame($exception, $reason);
}
}

/**
* @depends testYieldPendingPromise
* @depends testGeneratorThrowingExceptionWithFinallyRejectsCoroutine
*/
public function testGeneratorThrowingExceptionWithFinallyBlockThrowing()
{
$exception = new Exception();

$generator = function () use (&$yielded, $exception) {
try {
throw new Exception();
} finally {
throw $exception;
}

yield; // Unreachable, but makes function a generator.
};

$coroutine = new Coroutine($generator());

$callback = $this->createCallback(1);
$callback->method('__invoke')
->with($this->identicalTo($exception));

$coroutine->done($this->createCallback(0), $callback);

Loop\run();

$this->assertTrue($coroutine->isRejected());

try {
$coroutine->wait();
} catch (Exception $reason) {
$this->assertSame($exception, $reason);
}
}

/**
* @depends testYieldScalar
Expand Down

0 comments on commit 789903e

Please sign in to comment.