diff --git a/composer.json b/composer.json
index f53002b..366a5cd 100644
--- a/composer.json
+++ b/composer.json
@@ -36,7 +36,7 @@
"ext-json": "*",
"phpunit/phpunit": "^9",
"jetbrains/phpstorm-stubs": "^2019.3",
- "psalm/phar": "6.20.*"
+ "psalm/phar": "^7.0.0-rc10"
},
"autoload": {
"psr-4": {
diff --git a/examples/benchmark-memory.php b/examples/benchmark-memory.php
index 491d3de..9210989 100644
--- a/examples/benchmark-memory.php
+++ b/examples/benchmark-memory.php
@@ -11,6 +11,10 @@
use Revolt\EventLoop;
use Revolt\EventLoop\Driver;
+use Revolt\EventLoop\Driver\EvDriver;
+use Revolt\EventLoop\Driver\EventDriver;
+use Revolt\EventLoop\Driver\StreamSelectDriver;
+use Revolt\EventLoop\Driver\UvDriver;
require __DIR__ . '/../vendor/autoload.php';
@@ -21,15 +25,17 @@
/** @psalm-suppress RiskyCast */
$t = (int) \round((\array_key_exists('t', $args) ? (int) $args['t'] : 0));
-if (\array_key_exists('d', $args)) {
- if (\is_string($args['d'])) {
- $loopClass = 'Revolt\EventLoop\Driver\\' . $args['d'] . 'Driver';
- if (\class_exists($loopClass)) {
- $loop = new ($loopClass)();
- if ($loop instanceof Driver) {
- EventLoop::setDriver($loop);
- }
- }
+if (\array_key_exists('d', $args) && \is_string($args['d'])) {
+ $loop = match ($args['d']) {
+ 'Ev' => EvDriver::isSupported() ? new EvDriver() : null,
+ 'Event' => EventDriver::isSupported() ? new EventDriver() : null,
+ 'StreamSelect' => new StreamSelectDriver(),
+ 'Uv' => UvDriver::isSupported() ? new UvDriver() : null,
+ default => null,
+ };
+
+ if ($loop instanceof Driver) {
+ EventLoop::setDriver($loop);
}
}
diff --git a/examples/fiber-local-automatic.php b/examples/fiber-local-automatic.php
index 7a8d0c7..014efca 100644
--- a/examples/fiber-local-automatic.php
+++ b/examples/fiber-local-automatic.php
@@ -13,12 +13,17 @@
* This might be used to log the current URL, authenticated user, or request identifier in an HTTP server.
*
* @psalm-suppress UnusedProperty
+ *
+ * @psalm-mutable
*/
final class Logger
{
private int $nextId = 1;
private FiberLocal $transactionId;
+ /**
+ * @psalm-capabilities read-props
+ */
public function __construct()
{
$this->transactionId = new FiberLocal(fn () => $this->nextId++);
diff --git a/examples/fiber-local-manual.php b/examples/fiber-local-manual.php
index 04bb7fd..5ffe25c 100644
--- a/examples/fiber-local-manual.php
+++ b/examples/fiber-local-manual.php
@@ -14,10 +14,13 @@
*
* @psalm-suppress UnusedProperty
*/
-final class Logger
+final class ManualLogger
{
private FiberLocal $transactionId;
+ /**
+ * @psalm-capabilities read-props
+ */
public function __construct()
{
$this->transactionId = new FiberLocal(fn () => throw new \Exception('Unknown transaction ID'));
@@ -45,7 +48,7 @@ public function log(string $message): void
}
}
-$logger = new Logger();
+$logger = new ManualLogger();
$logger->setTransactionId(1);
EventLoop::delay(1, static function () use ($logger) {
diff --git a/examples/http-client-suspension.php b/examples/http-client-suspension.php
index c15a3af..bcc3965 100644
--- a/examples/http-client-suspension.php
+++ b/examples/http-client-suspension.php
@@ -57,9 +57,15 @@ function fetch(string $url): string
return $buffer;
}
+/**
+ * Returns the header block of an HTTP response.
+ *
+ * @psalm-pure
+ */
function extractHeader(string $httpResponse): string
{
return \explode("\r\n\r\n", $httpResponse, 2)[0];
}
+// Printed on the CLI, where the header is text rather than HTML.
echo extractHeader(fetch('http://www.google.com/'));
diff --git a/psalm.xml b/psalm.xml
index fd49f97..9cee111 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -15,12 +15,6 @@
-
-
-
-
-
-
@@ -98,6 +92,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/EventLoop.php b/src/EventLoop.php
index a6b1fa1..6868b50 100644
--- a/src/EventLoop.php
+++ b/src/EventLoop.php
@@ -16,6 +16,9 @@
/**
* Accessor to allow global access to the event loop.
*
+ * Scheduling and suspension are `io` for callers, like `usleep()`: the driver is the scheduler, and
+ * what other fibers do while the current one is suspended is their own effect.
+ *
* @see Driver
*/
final class EventLoop
@@ -35,24 +38,36 @@ public static function setDriver(Driver $driver): void
try {
/** @psalm-suppress InternalClass */
self::$driver = new class () extends AbstractDriver {
+ /**
+ * @psalm-pure
+ */
#[\Override]
protected function activate(array $callbacks): void
{
throw new \Error("Can't activate callback during garbage collection.");
}
+ /**
+ * @psalm-pure
+ */
#[\Override]
protected function dispatch(bool $blocking): void
{
throw new \Error("Can't dispatch during garbage collection.");
}
+ /**
+ * @psalm-capabilities read-props
+ */
#[\Override]
protected function deactivate(DriverCallback $callback): void
{
// do nothing
}
+ /**
+ * @psalm-pure
+ */
#[\Override]
public function getHandle(): mixed
{
@@ -83,6 +98,9 @@ protected function now(): float
*
* @param \Closure(...):void $closure The callback to queue.
* @param mixed ...$args The callback arguments.
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function queue(\Closure $closure, mixed ...$args): void
{
@@ -102,6 +120,9 @@ public static function queue(\Closure $closure, mixed ...$args): void
* invalidated before the callback invocation.
*
* @return string A unique identifier that can be used to cancel, enable or disable the callback.
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function defer(\Closure $closure): string
{
@@ -122,6 +143,9 @@ public static function defer(\Closure $closure): string
* before the callback invocation.
*
* @return string A unique identifier that can be used to cancel, enable or disable the callback.
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function delay(float $delay, \Closure $closure): string
{
@@ -142,6 +166,9 @@ public static function delay(float $delay, \Closure $closure): string
* @param \Closure(string):void $closure The callback to repeat.
*
* @return string A unique identifier that can be used to cancel, enable or disable the callback.
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function repeat(float $interval, \Closure $closure): string
{
@@ -165,6 +192,9 @@ public static function repeat(float $interval, \Closure $closure): string
* @param \Closure(string, resource):void $closure The callback to execute.
*
* @return string A unique identifier that can be used to cancel, enable or disable the callback.
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function onReadable(mixed $stream, \Closure $closure): string
{
@@ -188,6 +218,9 @@ public static function onReadable(mixed $stream, \Closure $closure): string
* @param \Closure(string, resource):void $closure The callback to execute.
*
* @return string A unique identifier that can be used to cancel, enable or disable the callback.
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function onWritable(mixed $stream, \Closure $closure): string
{
@@ -212,6 +245,9 @@ public static function onWritable(mixed $stream, \Closure $closure): string
* @return string A unique identifier that can be used to cancel, enable or disable the callback.
*
* @throws UnsupportedFeatureException If signal handling is not supported.
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function onSignal(int $signal, \Closure $closure): string
{
@@ -229,6 +265,9 @@ public static function onSignal(int $signal, \Closure $closure): string
* @return string The callback identifier.
*
* @throws InvalidCallbackError If the callback identifier is invalid.
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function enable(string $callbackId): string
{
@@ -247,6 +286,9 @@ public static function enable(string $callbackId): string
* @param string $callbackId The callback identifier.
*
* @return string The callback identifier.
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function disable(string $callbackId): string
{
@@ -260,6 +302,9 @@ public static function disable(string $callbackId): string
* callback is permanently invalid. Calling this function MUST NOT fail, even if passed an invalid identifier.
*
* @param string $callbackId The callback identifier.
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function cancel(string $callbackId): void
{
@@ -277,6 +322,9 @@ public static function cancel(string $callbackId): void
* @return string The callback identifier.
*
* @throws InvalidCallbackError If the callback identifier is invalid.
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function reference(string $callbackId): string
{
@@ -292,6 +340,9 @@ public static function reference(string $callbackId): string
* @param string $callbackId The callback identifier.
*
* @return string The callback identifier.
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function unreference(string $callbackId): string
{
@@ -352,6 +403,9 @@ public static function getType(string $callbackId): CallbackType
* @param string $callbackId The callback identifier.
*
* @return bool `true` if the callback is currently enabled, otherwise `false`.
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function isEnabled(string $callbackId): bool
{
@@ -364,6 +418,9 @@ public static function isEnabled(string $callbackId): bool
* @param string $callbackId The callback identifier.
*
* @return bool `true` if the callback is currently referenced, otherwise `false`.
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function isReferenced(string $callbackId): bool
{
@@ -391,6 +448,9 @@ public static function getDriver(): Driver
* Calls from the same fiber will return the same suspension object.
*
* @return Suspension
+ *
+ * @psalm-capabilities io
+ * @psalm-suppress ImpureMethodCall
*/
public static function getSuspension(): Suspension
{
@@ -415,6 +475,8 @@ public static function run(): void
* Disable construction as this is a static class.
*
* @psalm-suppress UnusedConstructor
+ *
+ * @psalm-capabilities read-props
*/
private function __construct()
{
diff --git a/src/EventLoop/Driver.php b/src/EventLoop/Driver.php
index 80464aa..ceea32c 100644
--- a/src/EventLoop/Driver.php
+++ b/src/EventLoop/Driver.php
@@ -7,6 +7,8 @@
/**
* The driver MUST run in its own fiber and execute callbacks in a separate fiber. If fibers are reused, the driver
* needs to call {@see FiberLocal::clear()} after running the callback.
+ *
+ * @psalm-mutable
*/
interface Driver
{
@@ -24,6 +26,8 @@ interface Driver
* error handler or exceptions that would be passed to an error handler but none exists to handle them.
*
* @throws \Error Thrown if the event loop is already running.
+ *
+ * @psalm-impure
*/
public function run(): void;
@@ -32,6 +36,8 @@ public function run(): void;
*
* When an event loop is stopped, it continues with its current tick and exits the loop afterwards. Multiple calls
* to stop MUST be ignored and MUST NOT raise an exception.
+ *
+ * @psalm-impure
*/
public function stop(): void;
@@ -41,11 +47,15 @@ public function stop(): void;
* Calls from the same fiber will return the same suspension object.
*
* @return Suspension
+ *
+ * @psalm-impure
*/
public function getSuspension(): Suspension;
/**
* @return bool True if the event loop is running, false if it is stopped.
+ *
+ * @psalm-impure
*/
public function isRunning(): bool;
@@ -60,6 +70,8 @@ public function isRunning(): bool;
*
* @param \Closure(...):void $closure The callback to queue.
* @param mixed ...$args The callback arguments.
+ *
+ * @psalm-impure
*/
public function queue(\Closure $closure, mixed ...$args): void;
@@ -76,6 +88,8 @@ public function queue(\Closure $closure, mixed ...$args): void;
* callback invocation.
*
* @return string A unique identifier that can be used to cancel, enable or disable the callback.
+ *
+ * @psalm-impure
*/
public function defer(\Closure $closure): string;
@@ -93,6 +107,8 @@ public function defer(\Closure $closure): string;
* callback invocation.
*
* @return string A unique identifier that can be used to cancel, enable or disable the callback.
+ *
+ * @psalm-impure
*/
public function delay(float $delay, \Closure $closure): string;
@@ -110,6 +126,8 @@ public function delay(float $delay, \Closure $closure): string;
* @param \Closure(string):void $closure The callback to repeat.
*
* @return string A unique identifier that can be used to cancel, enable or disable the callback.
+ *
+ * @psalm-impure
*/
public function repeat(float $interval, \Closure $closure): string;
@@ -130,6 +148,8 @@ public function repeat(float $interval, \Closure $closure): string;
* @param \Closure(string, resource):void $closure The callback to execute.
*
* @return string A unique identifier that can be used to cancel, enable or disable the callback.
+ *
+ * @psalm-impure
*/
public function onReadable(mixed $stream, \Closure $closure): string;
@@ -150,6 +170,8 @@ public function onReadable(mixed $stream, \Closure $closure): string;
* @param \Closure(string, resource):void $closure The callback to execute.
*
* @return string A unique identifier that can be used to cancel, enable or disable the callback.
+ *
+ * @psalm-impure
*/
public function onWritable(mixed $stream, \Closure $closure): string;
@@ -171,6 +193,8 @@ public function onWritable(mixed $stream, \Closure $closure): string;
* @return string A unique identifier that can be used to cancel, enable or disable the callback.
*
* @throws UnsupportedFeatureException If signal handling is not supported.
+ *
+ * @psalm-impure
*/
public function onSignal(int $signal, \Closure $closure): string;
@@ -185,6 +209,8 @@ public function onSignal(int $signal, \Closure $closure): string;
* @return string The callback identifier.
*
* @throws InvalidCallbackError If the callback identifier is invalid.
+ *
+ * @psalm-impure
*/
public function enable(string $callbackId): string;
@@ -195,6 +221,8 @@ public function enable(string $callbackId): string;
* callback is permanently invalid. Calling this function MUST NOT fail, even if passed an invalid identifier.
*
* @param string $callbackId The callback identifier.
+ *
+ * @psalm-impure
*/
public function cancel(string $callbackId): void;
@@ -210,6 +238,8 @@ public function cancel(string $callbackId): void;
* @param string $callbackId The callback identifier.
*
* @return string The callback identifier.
+ *
+ * @psalm-impure
*/
public function disable(string $callbackId): string;
@@ -224,6 +254,8 @@ public function disable(string $callbackId): string;
* @return string The callback identifier.
*
* @throws InvalidCallbackError If the callback identifier is invalid.
+ *
+ * @psalm-impure
*/
public function reference(string $callbackId): string;
@@ -236,6 +268,8 @@ public function reference(string $callbackId): string;
* @param string $callbackId The callback identifier.
*
* @return string The callback identifier.
+ *
+ * @psalm-impure
*/
public function unreference(string $callbackId): string;
@@ -250,6 +284,8 @@ public function unreference(string $callbackId): string;
*
* @param null|\Closure(\Throwable):void $errorHandler The callback to execute. `null` will clear the current
* handler.
+ *
+ * @psalm-impure
*/
public function setErrorHandler(?\Closure $errorHandler): void;
@@ -257,6 +293,8 @@ public function setErrorHandler(?\Closure $errorHandler): void;
* Gets the error handler closure or {@code null} if none is set.
*
* @return null|\Closure(\Throwable):void The previous handler, `null` if there was none.
+ *
+ * @psalm-impure
*/
public function getErrorHandler(): ?\Closure;
@@ -270,6 +308,8 @@ public function getErrorHandler(): ?\Closure;
* instance.
*
* @return null|object|resource The loop handle the event loop operates on. `null` if there is none.
+ *
+ * @psalm-impure
*/
public function getHandle(): mixed;
@@ -277,6 +317,8 @@ public function getHandle(): mixed;
* Returns all registered non-cancelled callback identifiers.
*
* @return string[] Callback identifiers.
+ *
+ * @psalm-impure
*/
public function getIdentifiers(): array;
@@ -286,6 +328,8 @@ public function getIdentifiers(): array;
* @param string $callbackId The callback identifier.
*
* @return CallbackType The callback type.
+ *
+ * @psalm-impure
*/
public function getType(string $callbackId): CallbackType;
@@ -295,6 +339,8 @@ public function getType(string $callbackId): CallbackType;
* @param string $callbackId The callback identifier.
*
* @return bool `true` if the callback is currently enabled, otherwise `false`.
+ *
+ * @psalm-impure
*/
public function isEnabled(string $callbackId): bool;
@@ -304,6 +350,8 @@ public function isEnabled(string $callbackId): bool;
* @param string $callbackId The callback identifier.
*
* @return bool `true` if the callback is currently referenced, otherwise `false`.
+ *
+ * @psalm-impure
*/
public function isReferenced(string $callbackId): bool;
@@ -313,6 +361,8 @@ public function isReferenced(string $callbackId): bool;
* If this method isn't implemented, dumping the event loop in a busy application, even indirectly, is a pain.
*
* @return array
+ *
+ * @psalm-impure
*/
public function __debugInfo(): array;
}
diff --git a/src/EventLoop/Driver/EvDriver.php b/src/EventLoop/Driver/EvDriver.php
index 9699664..60caa25 100644
--- a/src/EventLoop/Driver/EvDriver.php
+++ b/src/EventLoop/Driver/EvDriver.php
@@ -19,6 +19,9 @@ final class EvDriver extends AbstractDriver
/** @var array|null */
private static ?array $activeSignals = null;
+ /**
+ * @psalm-pure
+ */
public static function isSupported(): bool
{
return \extension_loaded("ev");
diff --git a/src/EventLoop/Driver/EventDriver.php b/src/EventLoop/Driver/EventDriver.php
index 36b6040..02e0a50 100644
--- a/src/EventLoop/Driver/EventDriver.php
+++ b/src/EventLoop/Driver/EventDriver.php
@@ -19,6 +19,9 @@ final class EventDriver extends AbstractDriver
/** @var array|null */
private static ?array $activeSignals = null;
+ /**
+ * @psalm-pure
+ */
public static function isSupported(): bool
{
return \extension_loaded("event");
diff --git a/src/EventLoop/Driver/StreamSelectDriver.php b/src/EventLoop/Driver/StreamSelectDriver.php
index d072ff3..1f634cd 100644
--- a/src/EventLoop/Driver/StreamSelectDriver.php
+++ b/src/EventLoop/Driver/StreamSelectDriver.php
@@ -53,7 +53,10 @@ public function __construct()
&& \function_exists('pcntl_signal_dispatch')
&& \function_exists('pcntl_signal');
- $this->streamSelectErrorHandler = function (int $errno, string $message): void {
+ $this->streamSelectErrorHandler = /**
+ * @psalm-capabilities read-props|write-this-props|write-refs
+ */
+ function (int $errno, string $message): void {
// Casing changed in PHP 8 from 'unable' to 'Unable'
if (\stripos($message, "stream_select(): unable to select [4]: ") === 0) { // EINTR
$this->streamSelectIgnoreResult = true;
@@ -102,6 +105,9 @@ public function onSignal(int $signal, \Closure $closure): string
return parent::onSignal($signal, $closure);
}
+ /**
+ * @psalm-pure
+ */
#[\Override]
public function getHandle(): mixed
{
diff --git a/src/EventLoop/Driver/TracingDriver.php b/src/EventLoop/Driver/TracingDriver.php
index 1888e99..4951f63 100644
--- a/src/EventLoop/Driver/TracingDriver.php
+++ b/src/EventLoop/Driver/TracingDriver.php
@@ -207,6 +207,9 @@ public function getHandle(): mixed
return $this->driver->getHandle();
}
+ /**
+ * @psalm-capabilities read-props
+ */
public function dump(): string
{
$dump = "Enabled, referenced callbacks keeping the loop running: ";
@@ -260,11 +263,17 @@ public function queue(\Closure $closure, mixed ...$args): void
$this->driver->queue($closure, ...$args);
}
+ /**
+ * @psalm-capabilities read-props
+ */
private function getCreationTrace(string $callbackId): string
{
return $this->creationTraces[$callbackId] ?? 'No creation trace, yet.';
}
+ /**
+ * @psalm-capabilities read-props
+ */
private function getCancelTrace(string $callbackId): string
{
return $this->cancelTraces[$callbackId] ?? 'No cancellation trace, yet.';
@@ -284,6 +293,8 @@ private function getCancelTrace(string $callbackId): string
* }> $trace Output of `debug_backtrace()`.
*
* @return string Formatted stacktrace.
+ *
+ * @psalm-pure
*/
private function formatStacktrace(array $trace): string
{
diff --git a/src/EventLoop/Driver/UvDriver.php b/src/EventLoop/Driver/UvDriver.php
index 98c81dd..d213fba 100644
--- a/src/EventLoop/Driver/UvDriver.php
+++ b/src/EventLoop/Driver/UvDriver.php
@@ -14,6 +14,9 @@
final class UvDriver extends AbstractDriver
{
+ /**
+ * @psalm-pure
+ */
public static function isSupported(): bool
{
return \extension_loaded("uv");
@@ -129,6 +132,9 @@ public function getHandle(): mixed
return $this->handle;
}
+ /**
+ * @psalm-capabilities read-props
+ */
#[\Override]
protected function now(): float
{
@@ -138,6 +144,9 @@ protected function now(): float
return \uv_now($this->handle) / 1000;
}
+ /**
+ * @psalm-capabilities read-props
+ */
#[\Override]
protected function dispatch(bool $blocking): void
{
@@ -145,6 +154,9 @@ protected function dispatch(bool $blocking): void
\uv_run($this->handle, $blocking ? \UV::RUN_ONCE : \UV::RUN_NOWAIT);
}
+ /**
+ * @psalm-capabilities read-props|write-this-props|write-refs
+ */
#[\Override]
protected function activate(array $callbacks): void
{
@@ -213,6 +225,9 @@ protected function activate(array $callbacks): void
}
}
+ /**
+ * @psalm-capabilities read-props
+ */
#[\Override]
protected function deactivate(DriverCallback $callback): void
{
@@ -252,6 +267,9 @@ protected function deactivate(DriverCallback $callback): void
}
}
+ /**
+ * @psalm-pure
+ */
private function getStreamCallbackFlags(StreamCallback $callback): int
{
if ($callback instanceof StreamWritableCallback) {
diff --git a/src/EventLoop/FiberLocal.php b/src/EventLoop/FiberLocal.php
index 34dd261..395d87f 100644
--- a/src/EventLoop/FiberLocal.php
+++ b/src/EventLoop/FiberLocal.php
@@ -10,6 +10,8 @@
* Each instance stores data separately for each fiber. Usage examples include contextual logging data.
*
* @template T
+ *
+ * @api
*/
final class FiberLocal
{
diff --git a/src/EventLoop/Internal/AbstractDriver.php b/src/EventLoop/Internal/AbstractDriver.php
index ef7513b..8e6236b 100644
--- a/src/EventLoop/Internal/AbstractDriver.php
+++ b/src/EventLoop/Internal/AbstractDriver.php
@@ -131,6 +131,9 @@ public function stop(): void
$this->stopped = true;
}
+ /**
+ * @psalm-capabilities read-props
+ */
#[\Override]
public function isRunning(): bool
{
@@ -143,6 +146,9 @@ public function queue(\Closure $closure, mixed ...$args): void
$this->microtaskQueue->enqueue([$closure, $args]);
}
+ /**
+ * @psalm-capabilities read-props|write-this-props|write-refs
+ */
#[\Override]
public function defer(\Closure $closure): string
{
@@ -184,6 +190,9 @@ public function repeat(float $interval, \Closure $closure): string
return $timerCallback->id;
}
+ /**
+ * @psalm-capabilities read-props|write-this-props|write-refs
+ */
#[\Override]
public function onReadable(mixed $stream, \Closure $closure): string
{
@@ -195,6 +204,9 @@ public function onReadable(mixed $stream, \Closure $closure): string
return $streamCallback->id;
}
+ /**
+ * @psalm-capabilities read-props|write-this-props|write-refs
+ */
#[\Override]
public function onWritable($stream, \Closure $closure): string
{
@@ -333,6 +345,9 @@ public function getSuspension(): Suspension
return $suspension;
}
+ /**
+ * @psalm-capabilities read-props|write-this-props|write-refs
+ */
#[\Override]
public function setErrorHandler(?\Closure $errorHandler): void
{
@@ -345,6 +360,9 @@ public function getErrorHandler(): ?\Closure
return $this->errorHandler;
}
+ /**
+ * @psalm-capabilities read-props
+ */
#[\Override]
public function __debugInfo(): array
{
@@ -357,12 +375,18 @@ public function __debugInfo(): array
// @codeCoverageIgnoreEnd
}
+ /**
+ * @psalm-capabilities read-props
+ */
#[\Override]
public function getIdentifiers(): array
{
return \array_keys($this->callbacks);
}
+ /**
+ * @psalm-capabilities read-props
+ */
#[\Override]
public function getType(string $callbackId): CallbackType
{
@@ -371,6 +395,9 @@ public function getType(string $callbackId): CallbackType
return $callback->getType();
}
+ /**
+ * @psalm-capabilities read-props
+ */
#[\Override]
public function isEnabled(string $callbackId): bool
{
@@ -379,6 +406,9 @@ public function isEnabled(string $callbackId): bool
return $callback->enabled;
}
+ /**
+ * @psalm-capabilities read-props
+ */
#[\Override]
public function isReferenced(string $callbackId): bool
{
@@ -391,16 +421,22 @@ public function isReferenced(string $callbackId): bool
* Activates (enables) all the given callbacks.
*
* @param array $callbacks
+ *
+ * @psalm-impure
*/
abstract protected function activate(array $callbacks): void;
/**
* Dispatches any pending read/write, timer, and signal events.
+ *
+ * @psalm-impure
*/
abstract protected function dispatch(bool $blocking): void;
/**
* Deactivates (disables) the given callback.
+ *
+ * @psalm-impure
*/
abstract protected function deactivate(DriverCallback $callback): void;
@@ -436,6 +472,8 @@ final protected function error(\Closure $closure, \Throwable $exception): void
*
* Note this value does not necessarily correlate to wall-clock time, rather the value returned is meant to be used
* in relative comparisons to prior values returned by this method (intervals, expiration calculations, etc.).
+ *
+ * @psalm-impure
*/
abstract protected function now(): float;
@@ -464,6 +502,8 @@ private function invokeMicrotasks(): void
/**
* @return bool True if no enabled and referenced callbacks remain in the loop.
+ *
+ * @psalm-capabilities read-props
*/
private function isEmpty(): bool
{
@@ -538,6 +578,8 @@ private function invokeCallbacks(): void
/**
* @param \Closure():mixed $interrupt
+ *
+ * @psalm-capabilities read-props|write-this-props|write-refs
*/
private function setInterrupt(\Closure $interrupt): void
{
@@ -546,6 +588,9 @@ private function setInterrupt(\Closure $interrupt): void
$this->interrupt = $interrupt;
}
+ /**
+ * @psalm-capabilities read-props|write-this-props|write-refs
+ */
private function invokeInterrupt(): void
{
\assert($this->interrupt !== null);
@@ -663,6 +708,9 @@ private function createErrorCallback(): void
};
}
+ /**
+ * @psalm-capabilities read-props|write-this-props|write-refs
+ */
private function callbackId(): string
{
$callbackId = $this->nextId;
@@ -677,6 +725,9 @@ private function callbackId(): string
return $callbackId;
}
+ /**
+ * @psalm-pure
+ */
final public function __serialize(): never
{
throw new \Error(__CLASS__ . ' does not support serialization');
@@ -684,6 +735,8 @@ final public function __serialize(): never
/**
* @psalm-suppress UnusedParam $data param required.
+ *
+ * @psalm-pure
*/
final public function __unserialize(array $data): never
{
diff --git a/src/EventLoop/Internal/ClosureHelper.php b/src/EventLoop/Internal/ClosureHelper.php
index 48f8fa2..da7478b 100644
--- a/src/EventLoop/Internal/ClosureHelper.php
+++ b/src/EventLoop/Internal/ClosureHelper.php
@@ -4,9 +4,16 @@
namespace Revolt\EventLoop\Internal;
-/** @internal */
+/**
+ * @internal
+ *
+ * @psalm-immutable
+ */
final class ClosureHelper
{
+ /**
+ * @psalm-capabilities read-props
+ */
public static function getDescription(\Closure $closure): string
{
try {
diff --git a/src/EventLoop/Internal/DeferCallback.php b/src/EventLoop/Internal/DeferCallback.php
index b2bd843..21041ae 100644
--- a/src/EventLoop/Internal/DeferCallback.php
+++ b/src/EventLoop/Internal/DeferCallback.php
@@ -9,6 +9,9 @@
/** @internal */
final class DeferCallback extends DriverCallback
{
+ /**
+ * @psalm-pure
+ */
#[\Override]
public function getType(): CallbackType
{
diff --git a/src/EventLoop/Internal/DriverCallback.php b/src/EventLoop/Internal/DriverCallback.php
index 8cb47bf..8e8e492 100644
--- a/src/EventLoop/Internal/DriverCallback.php
+++ b/src/EventLoop/Internal/DriverCallback.php
@@ -23,10 +23,15 @@ public function __construct(
) {
}
+ /**
+ * @psalm-capabilities read-props
+ */
abstract public function getType(): CallbackType;
/**
* @param string $property
+ *
+ * @psalm-pure
*/
public function __get(string $property): never
{
@@ -36,6 +41,8 @@ public function __get(string $property): never
/**
* @param string $property
* @param mixed $value
+ *
+ * @psalm-pure
*/
public function __set(string $property, mixed $value): never
{
diff --git a/src/EventLoop/Internal/DriverSuspension.php b/src/EventLoop/Internal/DriverSuspension.php
index e0edbc7..1b940e3 100644
--- a/src/EventLoop/Internal/DriverSuspension.php
+++ b/src/EventLoop/Internal/DriverSuspension.php
@@ -13,6 +13,8 @@
*
* @template T
* @implements Suspension
+ *
+ * @psalm-suppress ImmutableDependency The driver's bookkeeping and the fibers it runs are not the caller's effects.
*/
final class DriverSuspension implements Suspension
{
@@ -179,6 +181,9 @@ public function throw(\Throwable $throwable): void
}
}
+ /**
+ * @psalm-pure
+ */
private function formatStacktrace(array $trace): string
{
return \implode("\n", \array_map(static function ($e, $i) {
diff --git a/src/EventLoop/Internal/SignalCallback.php b/src/EventLoop/Internal/SignalCallback.php
index 33e02cf..5110cd9 100644
--- a/src/EventLoop/Internal/SignalCallback.php
+++ b/src/EventLoop/Internal/SignalCallback.php
@@ -9,6 +9,9 @@
/** @internal */
final class SignalCallback extends DriverCallback
{
+ /**
+ * @psalm-capabilities read-props
+ */
public function __construct(
string $id,
\Closure $closure,
@@ -17,6 +20,9 @@ public function __construct(
parent::__construct($id, $closure);
}
+ /**
+ * @psalm-pure
+ */
#[\Override]
public function getType(): CallbackType
{
diff --git a/src/EventLoop/Internal/StreamCallback.php b/src/EventLoop/Internal/StreamCallback.php
index 56a2547..292d20f 100644
--- a/src/EventLoop/Internal/StreamCallback.php
+++ b/src/EventLoop/Internal/StreamCallback.php
@@ -9,6 +9,8 @@ abstract class StreamCallback extends DriverCallback
{
/**
* @param resource $stream
+ *
+ * @psalm-capabilities read-props
*/
public function __construct(
string $id,
diff --git a/src/EventLoop/Internal/StreamReadableCallback.php b/src/EventLoop/Internal/StreamReadableCallback.php
index a68c793..39babee 100644
--- a/src/EventLoop/Internal/StreamReadableCallback.php
+++ b/src/EventLoop/Internal/StreamReadableCallback.php
@@ -9,6 +9,9 @@
/** @internal */
final class StreamReadableCallback extends StreamCallback
{
+ /**
+ * @psalm-pure
+ */
#[\Override]
public function getType(): CallbackType
{
diff --git a/src/EventLoop/Internal/StreamWritableCallback.php b/src/EventLoop/Internal/StreamWritableCallback.php
index a3c8626..23c3df8 100644
--- a/src/EventLoop/Internal/StreamWritableCallback.php
+++ b/src/EventLoop/Internal/StreamWritableCallback.php
@@ -9,6 +9,9 @@
/** @internal */
final class StreamWritableCallback extends StreamCallback
{
+ /**
+ * @psalm-pure
+ */
#[\Override]
public function getType(): CallbackType
{
diff --git a/src/EventLoop/Internal/TimerCallback.php b/src/EventLoop/Internal/TimerCallback.php
index 7b6d314..1fcdf5c 100644
--- a/src/EventLoop/Internal/TimerCallback.php
+++ b/src/EventLoop/Internal/TimerCallback.php
@@ -9,6 +9,9 @@
/** @internal */
final class TimerCallback extends DriverCallback
{
+ /**
+ * @psalm-capabilities read-props
+ */
public function __construct(
string $id,
public readonly float $interval,
@@ -19,6 +22,9 @@ public function __construct(
parent::__construct($id, $callback);
}
+ /**
+ * @psalm-capabilities read-props
+ */
#[\Override]
public function getType(): CallbackType
{
diff --git a/src/EventLoop/Internal/TimerQueue.php b/src/EventLoop/Internal/TimerQueue.php
index 8caef94..b070533 100644
--- a/src/EventLoop/Internal/TimerQueue.php
+++ b/src/EventLoop/Internal/TimerQueue.php
@@ -8,6 +8,8 @@
* Uses a binary tree stored in an array to implement a heap.
*
* @internal
+ *
+ * @psalm-capabilities read-props|write-this-props|write-refs
*/
final class TimerQueue
{
@@ -21,6 +23,8 @@ final class TimerQueue
* Inserts the callback into the queue.
*
* Time complexity: O(log(n)).
+ *
+ * @psalm-capabilities read-props|write-this-props|write-refs
*/
public function insert(TimerCallback $callback): void
{
@@ -37,6 +41,8 @@ public function insert(TimerCallback $callback): void
* Removes the given callback from the queue.
*
* Time complexity: O(log(n)).
+ *
+ * @psalm-capabilities read-props|write-this-props|write-refs
*/
public function remove(TimerCallback $callback): void
{
@@ -57,6 +63,8 @@ public function remove(TimerCallback $callback): void
* @param float $now Current event loop time.
*
* @return TimerCallback|null Expired callback at the top of the heap or null if the callback has not expired.
+ *
+ * @psalm-capabilities read-props|write-this-props|write-refs
*/
public function extract(float $now): ?TimerCallback
{
@@ -80,6 +88,8 @@ public function extract(float $now): ?TimerCallback
* Time complexity: O(1).
*
* @return float|null Expiration time of the callback at the top of the heap or null if the heap is empty.
+ *
+ * @psalm-capabilities read-props
*/
public function peek(): ?float
{
@@ -88,6 +98,8 @@ public function peek(): ?float
/**
* @param int $node Rebuild the data array from the given node upward.
+ *
+ * @psalm-capabilities read-props|write-this-props|write-refs
*/
private function heapifyUp(int $node): void
{
@@ -100,6 +112,8 @@ private function heapifyUp(int $node): void
/**
* @param int $node Rebuild the data array from the given node downward.
+ *
+ * @psalm-capabilities read-props|write-this-props|write-refs
*/
private function heapifyDown(int $node): void
{
@@ -122,6 +136,9 @@ private function heapifyDown(int $node): void
}
}
+ /**
+ * @psalm-capabilities read-props|write-this-props|write-refs
+ */
private function swap(int $left, int $right): void
{
$temp = $this->callbacks[$left];
@@ -135,6 +152,8 @@ private function swap(int $left, int $right): void
/**
* @param int $node Remove the given node and then rebuild the data array.
+ *
+ * @psalm-capabilities read-props|write-this-props|write-refs
*/
private function removeAndRebuild(int $node): void
{
diff --git a/src/EventLoop/InvalidCallbackError.php b/src/EventLoop/InvalidCallbackError.php
index 8be00e2..f1ea0e2 100644
--- a/src/EventLoop/InvalidCallbackError.php
+++ b/src/EventLoop/InvalidCallbackError.php
@@ -6,6 +6,11 @@
use Revolt\EventLoop\Internal\ClosureHelper;
+/**
+ * Error thrown for an invalid event-loop callback.
+ *
+ * @api
+ */
final class InvalidCallbackError extends \Error
{
public const E_NONNULL_RETURN = 1;
@@ -13,6 +18,8 @@ final class InvalidCallbackError extends \Error
/**
* MUST be thrown if any callback returns a non-null value.
+ *
+ * @psalm-capabilities read-props
*/
public static function nonNullReturn(string $callbackId, \Closure $closure): self
{
@@ -27,6 +34,8 @@ public static function nonNullReturn(string $callbackId, \Closure $closure): sel
* MUST be thrown if any operation (except disable() and cancel()) is attempted with an invalid callback identifier.
*
* An invalid callback identifier is any identifier that is not yet emitted by the driver or cancelled by the user.
+ *
+ * @psalm-pure
*/
public static function invalidIdentifier(string $callbackId): self
{
@@ -45,6 +54,8 @@ public static function invalidIdentifier(string $callbackId): self
/**
* @param string $callbackId The callback identifier.
* @param string $message The exception message.
+ *
+ * @psalm-capabilities read-props
*/
private function __construct(string $callbackId, int $code, string $message)
{
@@ -62,6 +73,9 @@ public function getCallbackId(): string
return $this->callbackId;
}
+ /**
+ * @psalm-capabilities read-props|write-this-props|write-refs
+ */
public function addInfo(string $key, string $message): void
{
$this->info[$key] = $message;
diff --git a/src/EventLoop/Suspension.php b/src/EventLoop/Suspension.php
index 216cfc1..5f1886b 100644
--- a/src/EventLoop/Suspension.php
+++ b/src/EventLoop/Suspension.php
@@ -21,11 +21,15 @@
* ```
*
* @template T
+ *
+ * @psalm-mutable
*/
interface Suspension
{
/**
* @param T $value The value to return from the call to {@see suspend()}.
+ *
+ * @psalm-capabilities io
*/
public function resume(mixed $value = null): void;
@@ -33,11 +37,15 @@ public function resume(mixed $value = null): void;
* Returns the value provided to {@see resume()} or throws the exception provided to {@see throw()}.
*
* @return T
+ *
+ * @psalm-capabilities io
*/
public function suspend(): mixed;
/**
* Throws the given exception from the call to {@see suspend()}.
+ *
+ * @psalm-capabilities io
*/
public function throw(\Throwable $throwable): void;
}
diff --git a/src/EventLoop/UncaughtThrowable.php b/src/EventLoop/UncaughtThrowable.php
index 2ee4c74..9f9886a 100644
--- a/src/EventLoop/UncaughtThrowable.php
+++ b/src/EventLoop/UncaughtThrowable.php
@@ -8,6 +8,9 @@
final class UncaughtThrowable extends \Error
{
+ /**
+ * @psalm-pure
+ */
public static function throwingCallback(\Closure $closure, \Throwable $previous): self
{
return new self(
@@ -17,11 +20,17 @@ public static function throwingCallback(\Closure $closure, \Throwable $previous)
);
}
+ /**
+ * @psalm-pure
+ */
public static function throwingErrorHandler(\Closure $closure, \Throwable $previous): self
{
return new self("Uncaught %s thrown in event loop error handler %s%s", $closure, $previous);
}
+ /**
+ * @psalm-capabilities read-props
+ */
private function __construct(string $message, \Closure $closure, \Throwable $previous)
{
parent::__construct(\sprintf(