symplely/coroutine

View on GitHub
Coroutine/Core.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php

declare(strict_types=1);

use Async\Co;
use Async\Defer;
use Async\Kernel;
use Async\Channel;
use Async\Coroutine;
use Async\CancelledError;
use Async\CoroutineInterface;
use Async\InvalidStateError;
use Async\Log;
use Async\Panic;
use Async\Panicking;
use Async\TaskInterface;
use Async\Misc\TaskGroup;
use Async\Misc\Semaphore;
use Async\Misc\AsyncIterator;
use Async\Misc\ContextInterface;
use Psr\Container\ContainerInterface;

use function Async\Worker\awaitable_future;

if (!\function_exists('coroutine_run')) {
  /**
   * A construct to _return_ an **associative** `array`, a dictionary.
   *
   * @param string|int $key
   * @param mixed $value
   * @return array
   */
  function kv($key, $value): array
  {
    return [$key => $value];
  }

  /**
   * Check for `async` logging system activation.
   *
   * @return boolean
   */
  function is_logging(): bool
  {
    return Log::isLogging();
  }

  /**
   * Turn `on/off` *system debugging* for source code __debugging_info()__ statement usage.
   *
   * @return void
   */
  function debugging(bool $onOff = false): void
  {
    CO::set('debugging', $onOff);
  }

  /**
   * Check for Coroutine `debugging` activation.
   *
   * @return boolean
   */
  function is_debugging(): bool
  {
    return Co::has('debugging') && Co::get('debugging') === true;
  }

  /**
   * Display `object/variable` or a `message` to console only, with additional system debugging info.
   * - `debugging(true)` must be _called and set_ to show system debugging info of __task/coroutine__ _running_ `state/status`.
   *
   * @param mixed $objectMessage
   * @param TaskInterface|null $task
   * @param CoroutineInterface|null $coroutine
   * @return void
   */
  function debugging_info($objectMessage = null, TaskInterface $task = null, CoroutineInterface $coroutine = null)
  {
    $isConsole = \IS_CLI && !\defined('CO_STDIN');
    if ($isConsole && !empty($objectMessage))
      \is_string($objectMessage) ? \printf('%s', $objectMessage) : \print_r($objectMessage);

    if (\is_debugging() && $isConsole)
      \var_dump($task, $coroutine);
  }

  /**
   * _Toggle_ or turn _on/off_ how **Coroutine** handle network operations.
   *
   * @param bool|string $uv
   * - `true` on - will use `libuv` features.
   * - `false` off - will use `PHP` native builtin routines.
   * @return void
   */
  function uv_native($uv = 'toggle')
  {
    Co::uvState($uv);
  }

  /**
   * Returns a random float between two numbers.
   *
   * Works similar to Python's `random.uniform()`
   * @see https://docs.python.org/3/library/random.html#random.uniform
   *
   * @param int $min
   * @param int $max
   * @return float
   */
  function random_uniform($min, $max)
  {
    return ($min + \lcg_value() * (\abs($max - $min)));
  }

  /**
   * Return the value (in fractional seconds) of a performance counter, i.e. a clock with the highest
   * available resolution to measure a short duration. Using either `hrtime` or system's `microtime`.
   *
   * @param string $tag
   * - A reference point used to set, to get the difference between the results of consecutive calls.
   * - Will be cleared/unset on the next consecutive call.
   *
   * @return float|null
   *
   * @see https://docs.python.org/3/library/time.html#time.perf_counter
   * @see https://nodejs.org/docs/latest-v11.x/api/console.html#console_console_time_label
   */
  function timer_for(string $tag = 'perf_counter')
  {
    if (Co::hasTiming($tag)) {
      $perf_counter = Co::getTiming($tag);
      Co::clearTiming($tag);
      return (float) (Co::hasTiming('hrtime') ? (\hrtime(true) / 1e+9) - $perf_counter : \microtime(true) - $perf_counter);
    }

    Co::setTiming($tag, (float) (Co::hasTiming('hrtime') ? \hrtime(true) / 1e+9 : \microtime(true)));
  }

  /**
   * Makes an resolvable function from `label` name that's callable with `coroutine_run()`, `go()`,
   * `await()`, `away()`, `spawner()` and inturn calls **create_task()**.
   * The passed in `function` is wrapped to be `awaitAble`. The `label` will be `Define()` and make that _name_ a **global** `constant`.
   *
   * - This will store a closure in `Co` static class with supplied `label` name as key.
   * @see https://docs.python.org/3.10/reference/compound_stmts.html#async-def
   *
   * @param string $label
   * @param callable $function
   * @return void
   * @throws Panic — if the **named** `label` function already exists.
   */
  function async(string $label, callable $function): void
  {
    if (!\defined("$label"))
      \define("$label", "$label");

    Kernel::async($label, $function);
  }

  /**
   * Allows convenient iteration over asynchronous `Iterator`.
   * This will obtain `task` results in the order that they complete, as they complete.
   * - Only `current()`, and `valid()` _methods_ SHOULD BE *implemented* in `Iterator` .
   * - This function needs to be prefixed with `yield`
   *
   * @param AsyncIterator $task A `task` producing _results_ in **chunks**.
   * @param \Closure $as Will **receive** _chunk_ of a _task_ `result` for processing.
   * @return void
   * @see https://docs.python.org/3/reference/compound_stmts.html#the-async-for-statement
   * @see https://docs.python.org/3.10/reference/expressions.html#asynchronous-generator-functions
   */
  function async_for(AsyncIterator $task, \Closure $as)
  {
    return Kernel::asyncFor($task, $as);
  }

  /**
   * Allows convenient iteration over asynchronous `Iterator`.
   * This will obtain `task` results in the order that they complete, as they complete.
   */
  \define('async_for', 'async_for');

  /**
   * Begins an asynchronous context manager that is able to suspend execution in its `__enter()` and `__exit()` methods.
   *  It is a **Error** to use `async_with` outside of an `async` function.
   * - This function needs to be prefixed with `yield`
   *
   * @see https://book.pythontips.com/en/latest/context_managers.html
   *
   * @param ContextInterface|resource $context
   * @param ContainerInterface|object|ContextInterface|null $object
   * @param array[] $options
   * @return ContextInterface
   * @throws Panic if no context instance, or `__enter()` method does not return `true`.
   */
  function async_with($context = null, $other = null, array $options = [])
  {
    return Kernel::asyncWith($context, $other, $options);
  }

  /**
   * Begins an asynchronous context manager that is able to suspend execution in its `__enter()` and `__exit()` methods.
   */
  \define('async_with', 'async_with');

  /**
   * Begins an asynchronous context manager that is able to suspend execution in its `__enter()` and `__exit()` methods.
   * It is a **Error** to use `with` outside of an `async` function.
   * - This function needs to be prefixed with `yield`
   *
   * @see https://book.pythontips.com/en/latest/context_managers.html
   *
   * @param ContextInterface|resource $context
   * @param \Closure $as - Will receive a **ContextInterface** instance, when finish will execute `__exit()`, the `ending()` function.
   * @return ContextInterface
   * @throws Panic if no context instance, or `__enter()` method does not return `true`.
   */
  function with($context = null, \Closure $as = null)
  {
    return Kernel::with($context, $as);
  }

  /**
   * Begins an asynchronous context manager that is able to suspend execution in its `__enter()` and `__exit()` methods.
   */
  \define('with', 'with');

  /**
   * Ends an `async_with()` or `with()` **Context** block, and executes `__exit()` method and any closing _routine_.
   * - This function needs to be prefixed with `yield`
   *
   * @param ContextInterface $context
   * @return void
   * @throws Exception if any `Context` _managed_ code **error's**.
   * @throws Panic if `__exit()` method does not return `true`.
   */
  function ending(ContextInterface $context)
  {
    try {
      if ($context() instanceof \Generator)
        yield $context();
    } finally {
      if ($context instanceof Semaphore) {
        yield $context->release();
      }

      if (!$context->exited())
        $context->__exit(new Panic('Context block failed to exit!'));
    }
  }

  /**
   * Ends an `async_with` or `with` **Context** block, and executes `__exit()` method and any closing _routine_.
   */
  \define('ending', 'ending');

  /**
   * A `TaskGroup` represents a collection of managed `tasks`. A group can be used to ensure that all tasks terminate together.
   *
   * @param array $tasks To monitor and collect results.
   * @param string $wait - When used as a context manager, will wait until
   * all contained tasks exit before moving on. The optional wait argument
   * specifies a strategy.
   *
   * If `wait=all` (the default), a task group waits for all tasks to exit.
   *
   * If `wait=any`, the group waits for the first task to exit.
   *
   * If `wait=object`, the group waits for the first task to return a non-None result.
   *
   * If `wait=None`, the group immediately cancels all running tasks.
   * @return TaskGroup
   * @source https://github.com/dabeaz/curio/blob/27ccf4d130dd8c048e28bd15a22015bce3f55d53/curio/task.py#L271
   */
  function task_group(array $tasks = [], $wait = 'all'): TaskGroup
  {
    return new TaskGroup($tasks, $wait);
  }

  /**
   * A `TaskGroup` represents a collection of managed `tasks`.
   * A group can be used to ensure that all tasks terminate together.
   */
  \define('task_group', 'task_group');

  /**
   * Returns `all` task _results_ (in `Task Id` creation order).
   *
   * @return array
   */
  function group_results(TaskGroup $object)
  {
    return $object->results();
  }

  /**
   * Returns `all` task _results_ (in `Task Id` creation order).
   */
  \define('group_results', 'group_results');

  /**
   * Returns _result_ of the `first` task to exit.
   *
   * @return mixed
   */
  function group_result(TaskGroup $object)
  {
    return $object->result();
  }

  /**
   * Returns _result_ of the `first` task to exit.
   */
  \define('group_result', 'group_result');

  /**
   * Returns the _result_ of a completed `task`.
   *
   * @param integer $tid task id instance
   * @return mixed
   * @throws Exception|Error if _task_ `erred`.
   * @throws InvalidStateError if still `running`, not terminated.
   */
  function result_for(int $tid)
  {
    return Kernel::resultFor($tid);
  }

  /**
   * Returns the _result_ of a completed `task`.
   */
  \define('result_for', 'result_for');

  /**
   * Returns the _exception_ of a `task`.
   *
   * @param integer $tid task id instance
   * @return null|Throwable
   * @throws InvalidStateError if _task_ still `running`, not terminated.
   */
  function exception_for(int $tid): ?\Throwable
  {
    return Kernel::exceptionFor($tid);
  }

  /**
   * Returns the _exception_ of a `task`.
   */
  \define('exception_for', 'exception_for');

  /**
   * Check _task_, returns `true` if cancelled.
   *
   * @param integer $tid task id instance
   * @return bool
   */
  function is_cancelled(int $tid): bool
  {
    return isset(\coroutine()->cancelledList()[$tid]);
  }

  /**
   * Check _task_, returns `true` if _currently_, **actively** being cancelled.
   *
   * @param integer $tid task id instance
   * @return bool
   */
  function is_cancelling(int $tid): bool
  {
    $task = \coroutine()->getTask($tid);
    if ($task instanceof TaskInterface  && $task->hasGroup() && $task->getGroup()->isWith())
      return $task->getGroup()->withTask()->exception() instanceof CancelledError;

    return $task instanceof TaskInterface  && $task->exception() instanceof CancelledError;
  }

  /**
   * Check _task_, returns `true` if terminated, not `running`.
   *
   * @param integer $tid task id instance
   * @return bool
   */
  function is_terminated(int $tid): bool
  {
    $coroutine = \coroutine();
    if ($coroutine->getTask($tid))
      return $coroutine->getTask($tid)->isFinished();
    elseif ($coroutine->isCompleted($tid))
      return $coroutine->getCompleted($tid)->isFinished();

    return false;
  }

  /**
   * Check _task_, returns `true` if joined, _execution status_ has changed.
   *
   * @param integer $tid task id instance
   * @return bool
   */
  function is_joined(int $tid): bool
  {
    $coroutine = \coroutine();
    if ($coroutine->getTask($tid))
      return $coroutine->getTask($tid)->isJoined();
    elseif ($coroutine->isCompleted($tid))
      return $coroutine->getCompleted($tid)->isJoined();

    return false;
  }

  /**
   * Run `callable(*args)` in a separate **process** and returns the result. In the event of _cancellation_,
   * the worker _process_ and the associated `task` is immediately terminated. This results in a `SIGKILL`
   * signal being sent to the worker _process_.
   *
   * The given `callable` executes in an entirely independent **PHP interpreter** and there is no shared global state.
   * - This function needs to be prefixed with `yield`
   *
   * @param callable $callable
   * @param mixed ...$args
   * @return mixed
   * @see https://curio.readthedocs.io/en/latest/reference.html?highlight=TaskError#run_in_process
   */
  function run_in_process(callable $callable, ...$args)
  {
    $process = function () use ($callable, $args) {
      return $callable(...$args);
    };

    return awaitable_future(function () use ($process) {
      return Kernel::addFuture($process, 0, false, null, null, \SIGKILL, null, 'signaling');
    });
  }

  /**
   * Run `callable(*args)` in a separate **process** and returns the result.
   * In the event of _cancellation_, the worker _process_ and the associated `task` is immediately terminated.
   * This results in a `SIGKILL` signal being sent to the worker _process_.
   */
  \define('run_in_process', 'run_in_process');

  /**
   * Run `callable(*args)` in a separate **thread** and return the result. If the calling task is cancelled,
   * the underlying worker thread (if started) is set aside and sent a termination request. Once it start running,
   * it will run fully to completion as a kind of zombie.
   *
   * The given `callable` executes in an `thread` and requires a `ZTS` **PHP** version.
   * - This function needs to be prefixed with `yield`
   *
   * @param callable $callable
   * @param mixed ...$args
   * @return mixed
   * @see https://curio.readthedocs.io/en/latest/reference.html?highlight=TaskError#run_in_thread
   */
  function run_in_thread(callable $callable, ...$args)
  {
    return awaitable_future(function () use ($callable, $args) {
      return Kernel::addThread($callable, ...$args);
    });
  }

  /**
   * Run `callable(*args)` in a separate **thread** and return the result. If the calling task is cancelled,
   * the underlying worker thread (if started) is set aside and sent a termination request.
   */
  \define('run_in_thread', 'run_in_thread');

  /**
   * This function will `pause` and execute the `label` function, with `arguments`,
   * only functions created with `async`, or some **reserved**,  or
   * a `PHP` builtin callable will work, anything else will throw `Panic` exception.
   * If `label` is a `PHP` builtin _command/function_ it will execute asynchronously in a **child/subprocess**,
   * by `proc_open`, or `uv_spawn` if **libuv** is loaded.
   *
   * - This function needs to be prefixed with `yield`
   *
   * @see https://www.python.org/dev/peps/pep-0492/#id56
   * @see https://docs.python.org/3.10/reference/expressions.html#await
   *
   * @param string $label `async` function, **reserved** or `PHP` builtin function.
   * @param mixed ...$args
   * @return mixed
   * @throws Panic if the **named** `label` function does not exists.
   */
  function await(string $label, ...$args)
  {
    return Kernel::await($label, ...$args);
  }

  /**
   * This function will `pause` and execute the `label` function, with `arguments`,
   * only functions created with `async`, or some **reserved**,  or
   * a `PHP` builtin callable will work, anything else will throw `Panic` exception.
   */
  \define('await', 'await');

  /**
   * Wrap the **result** with `yield`, or create a `Coroutine::value` object instance of it.
   *
   * Of which will signal and insure the actual return `value/result` is properly picked up.
   * - This should mostly be used within a `async` function if needed.
   * - Mostly when returning `values/results` where `yield` was not used within that code block.
   *
   * use as: `return value($result);`
   *
   * @param mixed $result
   * @param bool $byObject - return a `Coroutine::value` **object**
   * @return mixed
   *
   * @internal
   */
  function value($result, bool $byObject = true)
  {
    if ($byObject) {
      return yield Coroutine::value($result);
    } else {
      yield;
      return yield $result;
    }
  }

  /**
   * Wrap the **result** with `yield`, or create a `Coroutine::value` object instance of it.
   */
  \define('value', 'value');

  /**
   * **Schedule** an `async`, a coroutine _function_ for execution.
   * - This function needs to be prefixed with `yield`
   *
   * @see https://curio.readthedocs.io/en/latest/reference.html#tasks
   * @see https://docs.python.org/3.10/library/asyncio-task.html#creating-tasks
   * @source https://github.com/python/cpython/blob/11909c12c75a7f377460561abc97707a4006fc07/Lib/asyncio/tasks.py#L331
   *
   * @param Generator|callable|string $awaitableFunction - `async`, a coroutine, or a function to make `awaitable`
   * @param mixed ...$args - if **$awaitableFunction** is `Generator`, $args can hold `customState`, and `customData`
   * - for third party code integration.
   *
   * @return int $task id
   */
  function away($awaitableFunction, ...$args)
  {
    return Kernel::away($awaitableFunction, ...$args);
  }

  /**
   * Create a new task that concurrently executes the `async` function.
   */
  \define('create_task', 'away');

  /**
   * Run awaitable objects in the tasks set concurrently and block until the condition specified by race.
   *
   * Controls how the `gather()` function operates.
   * `gather_wait` will behave like **Promise** functions `All`, `Some`, `Any` in JavaScript.
   *
   * @param array<int|\Generator> $tasks
   * @param int $race - If set, initiate a competitive race between multiple tasks.
   * - When amount of tasks as completed, the `gather` will return with task results.
   * - When `0` (default), will wait for all to complete.
   * @param bool $exception - If `true` (default), the first raised exception is immediately
   *  propagated to the task that awaits on gather().
   * Other awaitables in the aws sequence won't be cancelled and will continue to run.
   * - If `false`, exceptions are treated the same as successful results, and aggregated in the result list.
   * @param bool $clear - If `true`, close/cancel remaining results, `false` (default)
   * @throws \LengthException - If the number of tasks less than the desired $race count.
   *
   * @see https://docs.python.org/3.7/library/asyncio-task.html#waiting-primitives
   *
   * @return array associative `$taskId` => `$result`
   */
  function gather_wait(array $tasks, int $race = 0, bool $exception = true, bool $clear = false)
  {
    return Kernel::gatherWait($tasks, $race, $exception, $clear);
  }

  /**
   * Run awaitable objects in the tasks set concurrently and block until the condition specified by race.
   */
  \define('gather_wait', 'gather_wait');

  /**
   * Run awaitable objects in the taskId sequence concurrently.
   * If any awaitable in taskId is a coroutine, it is automatically scheduled as a Task.
   *
   * If all awaitables are completed successfully, the result is an aggregate list of returned values.
   * The order of result values corresponds to the order of awaitables in taskId.
   *
   * The first raised exception is immediately propagated to the task that awaits on gather().
   * Other awaitables in the sequence won't be cancelled and will continue to run.
   *
   * @see https://docs.python.org/3.7/library/asyncio-task.html#asyncio.gather
   *
   * - This function needs to be prefixed with `yield`
   *
   * @param int|array $taskId
   * @return array[] associative `$taskId` => `$result`
   */
  function gather(...$taskId)
  {
    return Kernel::gather(...$taskId);
  }

  /**
   * Run awaitable objects in the taskId sequence concurrently.
   */
  \define('gather', 'gather');

  /**
   * Wrap the callable with `yield`, this insure the first attempt to execute will behave
   * like a generator function, will switch at least once without actually executing, return object instead.
   * - This function is used by `away()` and others, shouldn't really be called directly.
   *
   * @see https://docs.python.org/3.7/library/asyncio-task.html#awaitables
   * @source https://github.com/python/cpython/blob/11909c12c75a7f377460561abc97707a4006fc07/Lib/asyncio/tasks.py#L638
   *
   * @param callable $awaitableFunction
   * @param mixed $args
   *
   * @return \Generator
   *
   * @internal
   */
  function awaitable(callable $awaitableFunction = null, ...$args)
  {
    return yield yield $awaitableFunction(...$args);
  }

  /**
   * Similar to `awaitable`, but used mainly to delay scheduling or executing a regular function/method.
   * The executing code will be marked as `stateless`, not storing completion results afterwards.
   * The executing code is not supposed to be made for `yielding`.
   *
   * @param int $delay how many times to pause, _`yield` to event loop_, before executing `$function`
   * @param callable $function
   * @param mixed $args
   *
   * @return mixed
   *
   * @internal
   */
  function delayer(int $delay, callable $function, ...$args)
  {
    if ($delay > 0)
      foreach (\range(1, $delay) as $nan)
        yield;

    $result = $function(...$args);
    yield \stateless_task();

    return $result;
  }

  /**
   * Similar to `awaitable`, but used mainly to delay scheduling or executing a regular function/method.
   */
  \define('delayer', 'delayer');

  /**
   * Block/sleep for delay seconds.
   * Suspends the calling task, allowing other tasks to run.
   * - This function needs to be prefixed with `yield`
   *
   * @see https://docs.python.org/3.9/library/asyncio-task.html#sleeping
   * @source https://github.com/python/cpython/blob/bb0b5c12419b8fa657c96185d62212aea975f500/Lib/asyncio/tasks.py#L593
   *
   * @param float $delay
   * @param mixed $result - If provided, it is returned to the caller when the coroutine complete
   */
  function sleep_for(float $delay = 0.0, $result = null)
  {
    return Kernel::sleepFor($delay, $result);
  }

  /**
   * Suspends the calling task, allowing other tasks to run.
   */
  \define('sleep_for', 'sleep_for');

  /**
   * Suspends the calling task, allowing other tasks to run.
   */
  \define('sleep', 'sleep_for');

  /**
   * Wait for the `callable` to complete with a timeout.
   *
   * @see https://docs.python.org/3.10/library/asyncio-task.html#timeouts
   * @source https://github.com/python/cpython/blob/bb0b5c12419b8fa657c96185d62212aea975f500/Lib/asyncio/tasks.py#L392
   *
   * @param callable $callable
   * @param float $timeout
   * @return mixed
   * @throws TimeoutError If a timeout occurred into `current` task.
   * @throws CancelledError If a timeout occurred into `callable` task.
   */
  function wait_for($callable, float $timeout = 0.0)
  {
    return Kernel::waitFor($callable, $timeout);
  }

  /**
   * Wait for the callable to complete with a timeout.
   */
  \define('wait_for', 'wait_for');

  /**
   * Any blocking operation can be cancelled by a timeout.
   * Throws a `TaskTimeout` exception in the calling task after seconds have elapsed.
   * This function may be used in two ways. You can apply it to the execution of a single coroutine:
   *
   *```php
   *         yield timeout_after(seconds, coro(args))
   *
   * # Or you can use it as an asynchronous context manager to apply a timeout to a block of statements:
   *
   *         async_with(timeout_after(seconds));
   *               // Or
   *         yield with(timeout_after(seconds));
   *            yield coro1(args)
   *            yield coro2(args)
   *            ...
   *```
   * - This function needs to be prefixed with `yield`
   *
   * @param float $timeout
   * @param Generator|callable $callable
   * @param mixed ...$args
   * @return mixed
   * @throws TaskTimeout If a timeout has occurred.
   * @see https://curio.readthedocs.io/en/latest/reference.html#timeout_after
   * @source https://github.com/dabeaz/curio/blob/27ccf4d130dd8c048e28bd15a22015bce3f55d53/curio/time.py#L141
   */
  function timeout_after(float $timeout = 0.0, $callable = null, ...$args)
  {
    return Kernel::timeoutAfter($timeout, $callable, ...$args);
  }

  /**
   * Any blocking operation can be cancelled by a timeout.
   */
  \define('timeout_after', 'timeout_after');

  /**
   * **Schedule** an `async`, a coroutine _function_ for execution.
   * - This function needs to be prefixed with `yield`
   *
   * @see https://curio.readthedocs.io/en/latest/reference.html#tasks
   * @see https://docs.python.org/3.10/library/asyncio-task.html#creating-tasks
   * @source https://github.com/python/cpython/blob/11909c12c75a7f377460561abc97707a4006fc07/Lib/asyncio/tasks.py#L331
   *
   * @param Generator|callable|string $awaitableFunction - `async`, a coroutine, or a function to make `awaitable`
   * @param mixed ...$args - if **$awaitableFunction** is `Generator`, $args can hold `customState`, and `customData`
   * - for third party code integration.
   *
   * @return int $task id
   */
  function spawner($awaitableFunction, ...$args)
  {
    return yield Kernel::away($awaitableFunction, ...$args);
  }

  /**
   * **Schedule** an `async`, a coroutine _function_ for execution.
   */
  \define('spawn', 'spawner');

  /**
   * Wait for a task to terminate.
   * Returns the return value (if any) or throw a `Exception` if the task crashed with an exception.
   * - This function needs to be prefixed with `yield`
   *
   * @param integer $tid task id instance
   * @return mixed
   * @source https://github.com/dabeaz/curio/blob/27ccf4d130dd8c048e28bd15a22015bce3f55d53/curio/task.py#L177
   */
  function join_task(int $tid)
  {
    return yield Kernel::joinTask($tid);
  }

  /**
   *     Wait for the task to terminate and return its result.
   */
  \define('join_task', 'join_task');

  /**
   *     Wait for the task to terminate and return its result.
   */
  \define('join', 'join_task');

  /**
   * Create an new task.
   * - This function needs to be prefixed with `yield`
   *
   * @see https://docs.python.org/3.9/library/asyncio-task.html#creating-tasks
   * @source https://github.com/python/cpython/blob/11909c12c75a7f377460561abc97707a4006fc07/Lib/asyncio/tasks.py#L331
   *
   * @return int task ID
   */
  function create_task($awaitableFunction, ...$args)
  {
    return \away($awaitableFunction, ...$args);
  }

  /**
   * Cancel a task by **throwing** a `CancelledError` exception, this will also delay kill/remove
   * the task, the status of such can be checked with `is_cancelled` and `is_cancelling` functions.
   * Optionally pass custom cancel state and error message for third party code integration.
   * - This function needs to be prefixed with `yield`
   *
   * @see https://docs.python.org/3.10/library/asyncio-task.html#asyncio.Task.cancel
   * @source https://github.com/python/cpython/blob/bb0b5c12419b8fa657c96185d62212aea975f500/Lib/asyncio/tasks.py#L181
   *
   * @param int $tid task id instance
   * @param mixed $customState
   * @return bool
   *
   * @throws \InvalidArgumentException
   */
  function cancel_task(int $tid, $customState = null, string $errorMessage = 'Invalid task ID!')
  {
    return Kernel::cancelTask($tid, $customState, $errorMessage);
  }

  /**
   * Cancel a task by **throwing** a `CancelledError` exception, this will also delay kill/remove
   * the task, the status of such can be checked with `is_cancelled` and `is_cancelling` functions.
   */
  \define('cancel_task', 'cancel_task');


  /**
   * Cancel a task by **throwing** a `CancelledError` exception, this will also delay kill/remove
   * the task, the status of such can be checked with `is_cancelled` and `is_cancelling` functions.
   */
  \define('cancel', 'cancel_task');

  /**
   * Cancel _current_ `running` task by **throwing** a `CancelledError` exception, this will also delay kill/remove
   * `current` task, the status of such can be checked with `is_cancelled` and `is_cancelling` functions.
   * Optionally pass custom `cancel` state for third party code integration.
   *
   * - This function needs to be prefixed with `yield`
   * @param mixed $customState
   * @return bool
   */
  function kill_task($customState = null)
  {
    $currentTask = yield Kernel::currentTask();
    return yield Kernel::cancelTask($currentTask, $customState);
  }

  /**
   * Cancel _current_ `running` task by **throwing** a `CancelledError` exception, this will also delay kill/remove
   * `current` task, the status of such can be checked with `is_cancelled` and `is_cancelling` functions.
   */
  \define('kill_task', 'kill_task');

  /**
   * Cancel _current_ `running` task by **throwing** a `CancelledError` exception, this will also delay kill/remove
   * `current` task, the status of such can be checked with `is_cancelled` and `is_cancelling` functions.
   */
  \define('kill', 'kill_task');

  /**
   * Returns the current context task ID
   *
   * - This function needs to be prefixed with `yield`
   *
   * @return int task id instance
   */
  function current_task()
  {
    return Kernel::currentTask();
  }

  /**
   * Returns the current context task ID.
   */
  \define('current_task', 'current_task');

  /**
   * Set current context Task to stateless, meaning not storing any return values or exceptions on completion.
   * The task is not moved to completed task list.
   * This function will return the current context task ID.
   *
   * - This function needs to be prefixed with `yield`
   *
   * @return int
   */
  function stateless_task()
  {
    return \task_type('stateless');
  }

  /**
   * Set current context Task to stateless, meaning not storing any return values or exceptions on completion.
   */
  \define('stateless_task', 'stateless_task');

  /**
   * Set current context _Task_ to a **async** _`function/method`_. This is necessary to use `async_with()` and `with()`
   * inside _regular_ `function` or `class` methods and for the `task` to get _handled_ correctly on _errors_.
   *
   * This function will return the current context task ID.
   *
   * - This function needs to be prefixed with `yield`
   *
   * @return int
   */
  function method_task()
  {
    return \task_type('async_method');
  }

  /**
   * Set current Task context type, currently either `paralleled`, `async`, `awaited`, `async_method`, `threaded`,
   * `stateless`, or `monitored`.
   * Will return the current task ID.
   *
   * - This function needs to be prefixed with `yield`
   *
   * @param string $context
   * @return int
   */
  function task_type(string $context = 'async')
  {
    return Kernel::taskType($context);
  }

  /**
   * Performs a clean application shutdown, killing tasks/processes, and resetting all data, except **created** `async` functions.
   * - This function needs to be prefixed with `yield`
   *
   * Provide $skipTask incase called by an Signal Handler.
   *
   * @param int $skipTask - Defaults to the main parent task.
   * - The calling `$skipTask` task id will not get cancelled, the script execution will return to.
   * - Use `current_task()` to retrieve caller's task id.
   */
  function shutdown(int $skipTask = 1)
  {
    if (\is_logging()) {
      yield \logger_commit();
      yield \logger_shutdown();
    }

    return yield Kernel::shutdown($skipTask);
  }

  /**
   * Performs a clean application exit and shutdown.
   */
  \define('shutdown', 'shutdown');

  /**
   * Wait on read stream socket to be ready read from,
   * optionally schedule current task to execute immediately/next.
   *
   * - This function needs to be prefixed with `yield`
   */
  function read_wait($stream, bool $immediately = false)
  {
    return Kernel::readWait($stream, $immediately);
  }

  /**
   * Wait on read stream socket to be ready read from.
   */
  \define('read_wait', 'read_wait');

  /**
   * Wait on write stream socket to be ready to be written to,
   * optionally schedule current task to execute immediately/next.
   *
   * - This function needs to be prefixed with `yield`
   */
  function write_wait($stream, bool $immediately = false)
  {
    return Kernel::writeWait($stream, $immediately);
  }

  /**
   * Wait on write stream socket to be ready to be written to.
   */
  \define('write_wait', 'write_wait');

  /**
   * Wait on keyboard input.
   * Will not block other task on `Linux`, will continue other tasks until `enter` key is pressed,
   * Will block on Windows, once an key is typed/pressed, will continue other tasks `ONLY` if no key is pressed.
   * - This function needs to be prefixed with `yield`
   *
   * @return string
   */
  function input_wait(int $size = 256, bool $error = false)
  {
    return Coroutine::input($size, $error);
  }

  /**
   * Wait on keyboard input.
   */
  \define('input_wait', 'input_wait');

  /**
   * Returns current `Coroutine` Loop **instance**.
   *
   * @return CoroutineInterface|null
   */
  function coroutine(): ?CoroutineInterface
  {
    return Co::getLoop();
  }

  /**
   * Reset all `Coroutine` **global/static** `Co` variable data, including `async` functions defined.
   * Can also setup a task's unique `starting` id. This is mainly used for testing only.
   *
   * Just calling this function will also setup *order mode* for `Set` class, to **ordered** for testing purposes.
   *
   * @param boolean $unique
   * @param integer $starting - Set a fixed starting number, otherwise creates a cryptographically secure integer
   * @return void
   *
   * @codeCoverageIgnore
   */
  function coroutine_clear(bool $unique = true, int $starting = 0): void
  {
    $coroutine = \coroutine();
    if ($coroutine instanceof CoroutineInterface) {
      $coroutine->setup(false);
    }

    Co::reset();
    Co::resetAsync();
    Co::setUnique('dirty', 1);
    Co::setMode(true);
    Co::setUnique('max', ($unique ? \random_int(10000, 9999999999) : $starting));
  }

  /**
   * Creates a new task (using the next free task id), wraps **Generator**, a `coroutine` into a `Task` and schedule its execution.
   *
   * @see https://docs.python.org/3.10/library/asyncio-task.html#creating-tasks
   * @source https://github.com/python/cpython/blob/11909c12c75a7f377460561abc97707a4006fc07/Lib/asyncio/tasks.py#L331
   *
   * @param \Generator $routine
   * @param bool $isAsync should task type be set to a `async` function
   *
   * @return CoroutineInterface
   */
  function coroutine_create(\Generator $routine = null, bool $isAsync = false): CoroutineInterface
  {
    $coroutine = \coroutine();
    if (!$coroutine instanceof CoroutineInterface)
      $coroutine = new Coroutine();

    if (!empty($routine))
      $coroutine->createTask($routine, $isAsync);

    return $coroutine;
  }

  /**
   * This function runs the passed coroutine, taking care of managing the scheduler and
   * finalizing asynchronous generators. It should be used as a main entry point for programs, and
   * should ideally only be called once.
   *
   * @see https://docs.python.org/3.10/library/asyncio-task.html#asyncio.run
   * @see https://curio.readthedocs.io/en/latest/reference.html#basic-execution
   * @source https://github.com/python/cpython/blob/3.10/Lib/asyncio/runners.py
   *
   * @param generator|callable|string $routine - the **main** `coroutine` or `async` function.
   * @param mixed ...$args if **routine** is `async` function.
   * @throws Panic If **routine** not valid.
   */
  function coroutine_run($routine = null, ...$args): void
  {
    $isAsync = false;
    if (\is_string($routine) && Co::isFunction($routine)) {
      $isAsync = true;
      $routine = Co::getFunction($routine)(...$args);
    } elseif (\is_callable($routine)) {
      $routine = \awaitable($routine, ...$args);
    }

    if ($routine instanceof \Generator || empty($routine))
      \coroutine_create($routine, $isAsync)->run();
    else
      \panic("Invalid `coroutine` or no `async` function found!");
  }

  /**
   * Creates an communications Channel between coroutines.
   * Similar to Google Go language - basic, still needs additional functions
   * - This function needs to be prefixed with `yield`
   *
   * @return Channel $channel
   */
  function make()
  {
    return Kernel::make();
  }

  /**
   * Send message to an Channel
   * - This function needs to be prefixed with `yield`
   *
   * @param Channel $channel
   * @param mixed $message
   * @param int $taskId override send to different task, not set by `receiver()`
   */
  function sender(Channel $channel, $message = null, int $taskId = 0)
  {
    $noResult = yield Kernel::sender($channel, $message, $taskId);
    yield;
    return $noResult;
  }

  /**
   * Set task as Channel receiver, and wait to receive Channel message
   * - This function needs to be prefixed with `yield`
   *
   * @param Channel $channel
   */
  function receiver(Channel $channel)
  {
    yield Kernel::receiver($channel);
    $message = yield Kernel::receive($channel);
    return $message;
  }

  /**
   * A goroutine is a function that is capable of running concurrently with other functions.
   * To create a goroutine we use the keyword `go` followed by a function invocation
   * - This function needs to be prefixed with `yield`
   *
   * @see https://www.golang-book.com/books/intro/10#section1
   *
   * @param Generator|callable|string $function
   * @param mixed $args - if `generator`, $args can hold `customState`, and `customData`
   *
   * @return int task id
   */
  function go($function, ...$args)
  {
    return Kernel::away($function, ...$args);
  }

  /**
   * Modeled as in `Go` Language. The behavior of defer statements is straightforward and predictable.
   * There are three simple rules:
   * 1. *A deferred function's arguments are evaluated when the defer statement is evaluated.*
   * 2. *Deferred function calls are executed in Last In First Out order after the* surrounding function returns.
   * 3. *Deferred functions can`t modify return values when is type, but can modify content of reference to array or object.*
   *
   * PHP Limitations:
   * - In this *PHP* defer implementation,
   *  you cant modify returned value. You can modify only content of returned reference.
   * - You must always set first parameter in `defer` function,
   *  the parameter MUST HAVE same variable name as other `defer`,
   *  and this variable MUST NOT exist anywhere in local scope.
   * - You can`t pass function declared in local scope by name to *defer*.
   *
   * Modified from https://github.com/tito10047/php-defer
   *
   * @see https://golang.org/doc/effective_go.html#defer
   *
   * @param Defer|null $previous defer
   * @param callable $callback
   * @param mixed ...$args
   *
   * @throws \Exception
   */
  function defer(&$previous, $callback)
  {
    $args = \func_get_args();
    \array_shift($args);
    \array_shift($args);
    Defer::deferring($previous, $callback, $args);
  }

  /**
   * Modeled as in `Go` Language. Regains control of a panicking `task`.
   *
   * Recover is only useful inside `defer()` functions. During normal execution, a call to recover will return nil
   * and have no other effect. If the current `task` is panicking, a call to recover will capture the value given
   * to panic and resume normal execution.
   *
   * @param Defer|null $previous defer
   * @param callable $callback
   * @param mixed ...$args
   */
  function recover(&$previous, $callback)
  {
    $args = \func_get_args();
    \array_shift($args);
    \array_shift($args);
    Defer::recover($previous, $callback, $args);
  }

  /**
   * Modeled as in `Go` Language.
   *
   * An general purpose function for throwing an Coroutine `Exception`,
   * or some abnormal condition needing to keep an `Task` stack trace.
   *
   * @param string|Throwable $message or `new Exception($message)`
   * @param integer $code
   * @param \Throwable|null $previous
   * @throws Exception|Panic
   */
  function panic($message = '', $code = 0, \Throwable $previous = null)
  {
    if ($message instanceof Panicking)
      throw $message;

    throw new Panic($message, $code, $previous);
  }

  /**
   * Re-raises **throws** the `exception` that is currently being _handled_.
   * This function will also run the `shutdown` process, if it _detect's_ any issue getting current `task` or `coroutine` instance.
   * - This function needs to be prefixed with `yield`
   *
   * @throws Exception|Error
   * @see https://docs.python.org/3.10/reference/simple_stmts.html#raise
   * @codeCoverageIgnore
   */
  function raise()
  {
    try {
      $exception = \coroutine()->getTask(yield \current_task())->exception();
    } catch (\Throwable $th) {
      yield shutdown();
    }

    throw $exception;
  }

  /**
   * Re-raises **throws** the `exception` that is currently being _handled_.
   * This function will also run the `shutdown` process, if it _detect's_ any issue getting current `task` or `coroutine` instance.
   */
  \define('raise', 'raise');

  /**
   * This is useful as a `placeholder` when a statement is required syntactically.
   * - When executed, nothing happens.
   *
   * @return void
   * @see https://docs.python.org/3.10/reference/simple_stmts.html#pass
   */
  function pass(): void
  {
  }

  /**
   * This is useful as a `placeholder` when a statement is required syntactically.
   * - When executed, nothing happens.
   */
  \define('pass', 'pass');

  /**
   * An PHP Functional Programming Primitive.
   *
   * Return a curryied version of the given function. You can decide if you also
   * want to curry optional parameters or not.
   *
   * @see https://github.com/lstrojny/functional-php/blob/master/docs/functional-php.md#currying
   *
   * @param callable $function the function to curry
   * @param bool $required curry optional parameters ?
   * @return callable a curryied version of the given function
   */
  function curry(callable $function, $required = true)
  {
    $reflection = new \ReflectionFunction(\Closure::fromCallable($function));
    $count = $required ?
      $reflection->getNumberOfRequiredParameters() : $reflection->getNumberOfParameters();
    return \curry_n($count, $function);
  }

  /**
   * Return a version of the given function where the $count first arguments are curryied.
   *
   * No check is made to verify that the given argument count is either too low or too high.
   * If you give a smaller number you will have an error when calling the given function. If
   * you give a higher number, arguments will simply be ignored.
   *
   * @see https://github.com/lstrojny/functional-php/blob/master/docs/functional-php.md#curry_n
   *
   * @param int $count number of arguments you want to curry
   * @param callable $function the function you want to curry
   * @return callable a curryied version of the given function
   */
  function curry_n($count, callable $function)
  {
    $accumulator = function (array $arguments) use ($count, $function, &$accumulator) {
      return function (...$newArguments) use ($count, $function, $arguments, $accumulator) {
        $arguments = \array_merge($arguments, $newArguments);
        if ($count <= \count($arguments)) {
          return \call_user_func_array($function, $arguments);
        }
        return $accumulator($arguments);
      };
    };
    return $accumulator([]);
  }
}