diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..b45ddfb1d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,31 @@ +name: Documentation +on: + push: + branches: + - main + +permissions: + contents: read + pages: write + id-token: write + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + runs-on: ubuntu-latest + steps: + - uses: actions/configure-pages@v6 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: 3.x + - run: pip install zensical + - run: zensical build --clean + - uses: actions/upload-pages-artifact@v5 + with: + path: build/site + - uses: actions/deploy-pages@v5 + id: deployment diff --git a/.gitignore b/.gitignore index 41a66b83c..2e43ca3a3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,24 +4,6 @@ .cache/ build/ -_build/ -cmake-build-*/ -prefix/ -old/ -Testing/ - -docs/index.rst - -bench/data/ -*.svg - -mem.log -memory.csv -memory.*.csv -test.pdf -gmon.out -out.png -output.png **/.DS_Store diff --git a/.legacy/docs/_static/android-chrome-192x192.png b/.legacy/docs/_static/android-chrome-192x192.png deleted file mode 100644 index 20d2ba4eb..000000000 Binary files a/.legacy/docs/_static/android-chrome-192x192.png and /dev/null differ diff --git a/.legacy/docs/_static/android-chrome-512x512.png b/.legacy/docs/_static/android-chrome-512x512.png deleted file mode 100644 index de0ebe9ef..000000000 Binary files a/.legacy/docs/_static/android-chrome-512x512.png and /dev/null differ diff --git a/.legacy/docs/_static/apple-touch-icon.png b/.legacy/docs/_static/apple-touch-icon.png deleted file mode 100644 index 39e94423b..000000000 Binary files a/.legacy/docs/_static/apple-touch-icon.png and /dev/null differ diff --git a/.legacy/docs/_static/favicon-16x16.png b/.legacy/docs/_static/favicon-16x16.png deleted file mode 100644 index a3930b408..000000000 Binary files a/.legacy/docs/_static/favicon-16x16.png and /dev/null differ diff --git a/.legacy/docs/_static/favicon-32x32.png b/.legacy/docs/_static/favicon-32x32.png deleted file mode 100644 index 2d85ce719..000000000 Binary files a/.legacy/docs/_static/favicon-32x32.png and /dev/null differ diff --git a/.legacy/docs/_static/favicon.ico b/.legacy/docs/_static/favicon.ico deleted file mode 100644 index 6824cb2cc..000000000 Binary files a/.legacy/docs/_static/favicon.ico and /dev/null differ diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..24ee5b1be --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index edf767d82..000000000 --- a/docs/api.md +++ /dev/null @@ -1,287 +0,0 @@ -# libfork Public API - -All symbols live in the `lf` namespace. Access them via `import libfork;`. - ---- - -## Core concepts - -### `concept returnable` — `:task` - -`T` is `void` or a `std::movable` plain object type. Used as the return-type constraint on async functions. - -### `concept worker_stack` — `:concepts_stack` - -A type that provides a contiguous stack with `push`, `pop`, `checkpoint`, `prepare_release`, `release`, and `acquire`. - -### `concept lifo_stack` — `:concepts_context` - -`T` is a plain object type supporting `push(U)` and a `noexcept pop() -> U`. Used to define `worker_context`. - -### `concept worker_context` — `:concepts_context` - -A type that satisfies `lifo_stack>` and exposes a `worker_stack` via a `noexcept stack()`. - -### `using stack_t` — `:concepts_context` - -Extracts the stack type from a `worker_context`. - -### `concept has_context_typedef` — `:concepts_scheduler` - -`T` has a `context_type` member typedef. Used to define `scheduler` and constrain `context_t`. - -### `concept scheduler` — `:concepts_scheduler` - -A type satisfying `has_context_typedef` with a `post(sched_handle)` method. - -### `using context_t` — `:concepts_scheduler` - -Extracts `T::context_type`. Requires `has_context_typedef`. - -### `concept async_invocable` — `:concepts_invocable` - -`Fn` is callable with an `env` (or without one) and `Args...`, returning an `lf::task`. - -### `concept async_nothrow_invocable` — `:concepts_invocable` - -Subsumes `async_invocable` and requires the call to be `noexcept`. - -### `using async_result_t` — `:concepts_invocable` - -The `value_type` of the `task` returned by invoking `Fn`. - -### `concept async_invocable_to` — `:concepts_invocable` - -Subsumes `async_invocable` and constrains the result type to `R`. - -### `concept async_nothrow_invocable_to` — `:concepts_invocable` - -Subsumes both `async_nothrow_invocable` and `async_invocable_to`. - ---- - -## Coroutine types - -### `struct env` — `:task` - -The Y-combinator environment. Passed as the first argument to every async function, allowing recursive self-calls. Users declare it but never construct it directly. - -### `class task` — `:task` - -The return type of all async functions. `T` must satisfy `returnable`. Users never store or manipulate instances — the type exists solely to identify libfork coroutines. - ---- - -## Handles - -### `struct unsafe_steal_handle` — `:handles` - -Untyped steal handle. Used by `deque_policy` implementations to store handles without knowing the full context type. - -### `struct unsafe_sched_handle` — `:handles` - -Untyped schedule handle. Used when type-erasing across context types. - -### `struct steal_handle` — `:handles` - -Typed handle to a task suspended at a fork point; passed to `context.push()` and returned by `context.pop()` / `context.steal()`. - -### `struct sched_handle` — `:handles` - -Typed handle to a task ready to be started or resumed; passed to `scheduler::post()` and `execute()`. - ---- - -## Scope and task operations - -### `constexpr auto scope() -> scope_type` — `:ops` - -Primary entry point for fork/call/join. `co_await` this to obtain a `scope_ops` which provides: - -- `.fork(ret, fn, args...)` / `.fork(fn, args...)` / `.fork_drop(fn, args...)` — spawn concurrent child -- `.call(ret, fn, args...)` / `.call(fn, args...)` / `.call_drop(fn, args...)` — inline child call -- `.join()` — wait for all outstanding children - -### `constexpr auto child_scope() -> child_scope_type` — `:ops` - -Entry point for a cancellable scope. `co_await` this to obtain a `child_scope_ops`, which extends `scope_ops` and also inherits from `stop_source`. Tasks forked/called through this scope receive the scope's stop token. - ---- - -## Cancellation - -### `class stop_source` — `:stop` - -A non-copyable, non-movable stop source. Methods: `token()`, `stop_possible()`, `stop_requested()`, `request_stop()`, `race_request_stop()`. - -### `class stop_source::stop_token` — `:stop` - -Lightweight copyable token. Methods: `stop_possible()`, `stop_requested()`. - ---- - -## Scheduling - -### `class recv_state` — `:receiver` - -Pre-allocated shared state for a root task. Constructors mirror `make_shared` / `allocate_shared`: - -```cpp -recv_state s; // default-init -recv_state s{42}; // in-place init -recv_state s{std::allocator_arg, alloc}; // custom allocator -recv_state s{std::allocator_arg, alloc, 42}; // custom allocator + in-place init -recv_state s; // cancellable variant -``` - -Move-only. Pass to `schedule()` to get back a `receiver`. - -### `class receiver` — `:receiver` - -Handle to the result of a scheduled root task. Methods: - -- `.valid()` — whether the receiver is connected to state -- `.ready()` — whether the task has completed -- `.wait()` — block until complete (may be called multiple times) -- `.stop_source()` — access the stop source (only when `Stoppable = true`) -- `.get()` — consume the result, rethrowing any stored exception; throws `operation_cancelled_error` if cancelled - -### `auto schedule(Sch&&, recv_state, Fn&&, Args&&...) -> receiver` — `:schedule` - -Schedule an async function as a root task using a pre-allocated `recv_state`. - -### `auto schedule(Sch&&, Fn&&, Args&&...) -> receiver` — `:schedule` - -Convenience overload: default-constructs a non-cancellable `recv_state`. - -### `void execute(Context&, sched_handle)` — `:execute` - -Bind the calling thread to `context` and resume the scheduled task. Used by scheduler implementations. - -### `void execute(Context&, steal_handle)` — `:execute` - -Bind the calling thread to `context` and resume a stolen task. Used by scheduler implementations. - ---- - -## Polymorphic context base classes - -### `class base_context` — `:poly_context` - -CRTP base providing `stack()` -> `Stack&`. Inherit from this (or `poly_context`) when implementing a custom context. - -### `class poly_context` — `:poly_context` - -Abstract base for polymorphic contexts. Provides pure-virtual `push(steal_handle)`, `pop()`, and a defaulting `post(sched_handle)` that throws `post_error`. - ---- - -## Exception hierarchy - -All exceptions derive from `lf::libfork_exception : std::exception`. - -| Type | Thrown by | Condition | -| --------------------------- | ---------------------- | ---------------------------------------- | -| `libfork_exception` | — | Base type; catch-all for libfork errors | -| `schedule_error` | `schedule()` | Called from a worker thread | -| `execute_error` | `execute()` | Called from a worker thread | -| `steal_overflow_error` | `execute()` | A single task stolen > 65,535 times | -| `root_alloc_error` | `schedule()` | Root frame too large for inline buffer | -| `broken_receiver_error` | `receiver` methods | Receiver is in an invalid state | -| `operation_cancelled_error` | `receiver::get()` | Task was cancelled via stop token | -| `post_error` | `poly_context::post()` | Derived context does not override `post` | -| `deque_full_error` | `deque::push()` | Deque has reached maximum capacity | - ---- - -## Batteries: stacks - -All stacks satisfy `worker_stack`. Template parameter is an allocator for `std::byte`. - -### `class geometric_stack` — `:geometric_stack` - -Segmented stack with geometric growth and segment caching. Recommended default. - -### `class adaptor_stack` — `:adaptor_stack` - -Thin allocator-backed stack; allocates/deallocates on every push/pop. - -### `class slab_stack` — `:slab_stack` - -Fixed-capacity slab stack; throws on overflow. - ---- - -## Batteries: deque and adaptors - -### `class deque` — `:deque` - -Lock-free Chase-Lev work-stealing deque. `T` must be `lock_free` and `default_initializable`. Methods: `push(T)`, `pop() -> std::optional`, `get_thief() -> thief_handle`. - -### `class deque::thief_handle` — `:deque` - -Non-owning steal handle obtained via `deque::get_thief()`. Method: `steal(Fn on_empty) -> std::optional`. - -### `enum class err` — `:deque` - -Return code from low-level steal operations: `none`, `lost`, `empty`. - -### `struct steal_t` — `:deque` - -Steal result wrapper returned by `thief_handle::steal`. Has `err code` and `T val` fields; `operator bool` tests `code == err::none`. - -### `class adapt_vector` — `:adaptors` - -`std::vector`-backed LIFO deque policy. Satisfies `deque_policy`. - -### `class adapt_deque` — `:adaptors` - -Lock-free deque-backed policy. Satisfies both `deque_policy` and `stealable_deque_policy`. - ---- - -## Batteries: context policies and contexts - -### `concept deque_policy` — `:contexts` - -A type that is a LIFO stack over `unsafe_steal_handle` (has `push` and `pop`). - -### `concept stealable_deque_policy` — `:contexts` - -Extends `deque_policy` with a `steal() -> unsafe_steal_handle` method for FIFO work stealing. - -### `class mono_context` — `:contexts` - -Monomorphic worker context. Composes a `worker_stack` and a `deque_policy`. Satisfies `worker_context`. Exposes `steal()` when `Deque` satisfies `stealable_deque_policy`. - -### `class derived_poly_context` — `:contexts` - -Polymorphic worker context. Derives from `poly_context` and implements `push`/`pop` via `Deque`. Exposes `steal()` when `Deque` satisfies `stealable_deque_policy`. The `context_type` alias is `poly_context`. - ---- - -## Schedulers - -### `concept derived_worker_context` — `:inline_scheduler` - -`Context` has a `context_type` typedef and is derived from it (i.e., it is a concrete subclass of its own context type). - -### `class inline_scheduler` — `:inline_scheduler` - -Single-threaded synchronous scheduler. Stores one `Context` instance; `post()` calls `execute()` directly on the calling thread. - -### `enum class pool_kind` — `:basic_busy_pool` - -`mono` — uses `mono_context`; `poly` — uses `derived_poly_context`. - -### `class basic_busy_pool` — `:basic_busy_pool` - -Work-stealing thread pool using busy-wait. Spawns `N` worker threads (default: `std::thread::hardware_concurrency()`). Constructor: `basic_busy_pool(n_threads)`. - -### `using mono_busy_pool` — `:basic_busy_pool` - -Alias for `basic_busy_pool`. - -### `using poly_busy_pool` — `:basic_busy_pool` - -Alias for `basic_busy_pool`. diff --git a/docs/api/algorithm.md b/docs/api/algorithm.md new file mode 100644 index 000000000..fff952a3a --- /dev/null +++ b/docs/api/algorithm.md @@ -0,0 +1,121 @@ +--- +icon: lucide/workflow +--- + +# Algorithm + +The algorithm module provides fork-join algorithms over sized random-access +ranges. Algorithms are async functions and are normally launched with +`lf::schedule` or called from another task through a scope. + +## Concepts + +### `sized_random_access_range` + +`T` satisfies both `std::ranges::random_access_range` and +`std::ranges::sized_range`. + +## `for_each` + +### `inline constexpr for_each_impl for_each` + +Applies a function to every element in a sized random-access range or +iterator/sentinel pair. + +Overload shapes: + +```cpp +lf::for_each(env, first, last, chunk_size, fn); +lf::for_each(env, first, last, fn); +lf::for_each(env, range, chunk_size, fn); +lf::for_each(env, range, fn); +``` + +When `fn` is an async invocable, `for_each` calls it through libfork scopes. +Otherwise it invokes `fn` synchronously. + +```cpp +std::vector values{1, 2, 3}; +auto recv = lf::schedule(pool, lf::for_each, std::span(values), [](int& x) { + x *= 2; +}); +std::move(recv).get(); +``` + +The chunk-size overload assumes `chunk_size > 0`. Use the overload without a +chunk size for the single-element base case. + +## `fold` + +### `fold_chunk_error` + +Thrown when a public `fold` overload receives a non-positive chunk size. + +### `inline constexpr fold_fn fold` + +Reduces a sized random-access range or iterator/sentinel pair with a semigroup +operation. The result is `std::optional`: empty input returns `std::nullopt`, +and non-empty input returns the reduced value. + +Overload shapes: + +```cpp +lf::fold(env, first, last, chunk_size, binary_op, projection = {}); +lf::fold(env, first, last, binary_op, projection = {}); +lf::fold(env, range, chunk_size, binary_op, projection = {}); +lf::fold(env, range, binary_op, projection = {}); +``` + +The binary operation may be synchronous or async. The projection may also be +synchronous or async. The projected value and binary operation must satisfy +libfork's indirect semigroup constraints. + +```cpp +std::vector values{1, 2, 3, 4}; + +auto recv = lf::schedule(pool, lf::fold, std::span(values), std::plus<>{}); +std::optional sum = std::move(recv).get(); +``` + +With projection: + +```cpp +struct record { + int value; +}; + +std::vector records{{1}, {2}, {3}}; +auto recv = lf::schedule( + pool, + lf::fold, + std::span(records), + std::plus<>{}, + &record::value); +``` + +For overloads with an explicit chunk size, `chunk_size <= 0` throws +`fold_chunk_error` before checking whether the range is empty. + +## Async operations + +Both algorithms understand libfork async callables. For example, an async +projection can be used with `fold`: + +```cpp +struct square { + template + static auto operator()(lf::env, int value) -> lf::task { + co_return value * value; + } +}; + +auto recv = lf::schedule( + pool, + lf::fold, + std::span(values), + std::plus<>{}, + square{}); +``` + +If a callable is both synchronously and asynchronously invocable, the algorithm +implementation may prefer the async path where its constraints select it. diff --git a/docs/api/batteries.md b/docs/api/batteries.md new file mode 100644 index 000000000..381a040e8 --- /dev/null +++ b/docs/api/batteries.md @@ -0,0 +1,171 @@ +--- +icon: lucide/battery-charging +--- + +# Batteries + +Batteries provide ready-to-use stacks, deques, context policies, and worker +contexts. + +## Stacks + +All stack classes are allocator-aware templates over `std::byte` allocators and +satisfy `worker_stack` when the allocator's void pointer type is `void*`. + +### `geometric_stack>` + +Segmented user-space stack with geometric growth and one cached segment to +avoid hot splitting. This is the general-purpose stack choice. + +Main operations: + +- `empty() const noexcept -> bool` +- `checkpoint() noexcept` +- `push(std::size_t) -> void*` +- `pop(void*, std::size_t) noexcept -> void` +- `prepare_release() noexcept` +- `release(release_t) noexcept` +- `acquire(checkpoint_t) noexcept` + +### `slab_stack>` + +Fixed-size slab-backed stack. It allocates one slab at construction and throws +`std::bad_alloc` if a push exceeds the slab. + +Constructors: + +- `slab_stack()` +- `explicit slab_stack(diff_type num_nodes, Allocator const& = Allocator())` + +Main operations match `geometric_stack`. + +### `adaptor_stack>` + +Thin wrapper over an allocator. Every push allocates and every pop deallocates. +It is simple and useful for testing or comparison. + +Main operations match `geometric_stack`, except release/acquire only propagate +allocator state when needed. + +## Work-stealing deque + +### `concept dequeable` + +`T` can be stored in `lf::deque`. It must be lock-free and default-initializable. + +### `deque_full_error` + +Thrown when `deque::push` cannot add another element because the deque is full. + +### `enum class err` + +Result code for stealing: + +- `none`: steal succeeded. +- `lost`: another thief won the race. +- `empty`: the deque was empty. + +### `steal_t` + +Return type of `thief_handle::steal`. It supports `operator bool`, `operator*`, +and `operator->`. + +Fields: + +- `err code` +- `T val` + +`val` is valid only when `code == err::none`. + +### `deque>>` + +Bounded Chase-Lev single-producer multiple-consumer work-stealing deque. + +The owner thread uses: + +- `empty() const noexcept -> bool` +- `size() const noexcept -> size_type` +- `ssize() const noexcept -> diff_type` +- `capacity() const noexcept -> diff_type` +- `push(T) -> diff_type` +- `pop(Fn when_empty = {}) -> invoke_result_t` +- `thief() noexcept -> thief_handle` + +Non-owner threads steal through `deque::thief_handle`, which provides: + +- `empty() noexcept -> bool` +- `size() noexcept -> size_type` +- `ssize() noexcept -> diff_type` +- `capacity() noexcept -> diff_type` +- `steal() noexcept -> steal_t` + +All threads must stop using a deque before it is destroyed. + +## Deque adaptors + +### `adapt_vector>` + +`std::vector`-backed LIFO context policy. It supports: + +- `push(unsafe_steal_handle)` +- `pop() noexcept -> unsafe_steal_handle` + +Use it for inline/single-threaded contexts where stealing is unnecessary. + +### `adapt_deque>>` + +Lock-free deque-backed context policy. It supports: + +- `push(unsafe_steal_handle)` +- `pop() noexcept -> unsafe_steal_handle` +- `steal() noexcept -> unsafe_steal_handle` + +The default capacity is `32 * 1024` handles. The explicit constructor accepts a +capacity and allocator. + +## Context policies + +### `concept deque_policy` + +A LIFO stack over `unsafe_steal_handle`. Used by contexts to store work without +naming the full context type. + +### `concept stealable_deque_policy` + +Extends `deque_policy` with: + +```cpp +auto steal() -> lf::unsafe_steal_handle; +``` + +Use a stealable policy for multi-worker schedulers. + +## Contexts + +### `derived_poly_context` + +Polymorphic worker context composed of a `Stack` and `Deque`. It derives from +`poly_context`, implements virtual `push`/`pop`, and exposes `steal()` +when `Deque` is stealable. + +Aliases: + +- `context_type = poly_context` + +### `mono_context` + +Monomorphic worker context composed of a `Stack` and `Deque`. It implements +`push`/`pop` directly and exposes `steal()` when `Deque` is stealable. + +Aliases: + +- `context_type = mono_context` + +Both context classes support piecewise construction of their stack and deque: + +```cpp +lf::mono_context, lf::adapt_deque<>> ctx{ + std::piecewise_construct, + std::tuple{}, + std::tuple{1024}}; +``` diff --git a/docs/api/core/cancellation.md b/docs/api/core/cancellation.md new file mode 100644 index 000000000..f6d24316d --- /dev/null +++ b/docs/api/core/cancellation.md @@ -0,0 +1,128 @@ +--- +icon: lucide/octagon-x +--- + +# Cancellation + +Libfork cancellation is cooperative. `stop_source` records a request, and tasks +observe it through stop tokens propagated through scopes. Cancellation never +asynchronously interrupts running user code. + +## `stop_source` + +```cpp +class stop_source { + public: + class stop_token; + + constexpr stop_source() noexcept; + constexpr explicit stop_source(stop_token parent) noexcept; + + stop_source(stop_source const&) = delete; + stop_source(stop_source&&) = delete; + auto operator=(stop_source const&) -> stop_source& = delete; + auto operator=(stop_source&&) -> stop_source& = delete; + + auto token() const noexcept -> stop_token; + auto stop_requested() const noexcept -> bool; + auto request_stop() noexcept -> void; + auto race_request_stop() noexcept -> bool; +}; +``` + +`stop_source` is a small linked cancellation source. A source may be constructed +with a parent token; `stop_requested()` then checks this source and every parent +source in the chain. + +Root stoppability is created with [`recv_state`](receiver.md#recv_state). +Nested stoppability is created with [`child_scope()`](scope.md#child_scope). + +```cpp +template +auto root(lf::env) -> lf::task { + auto sc = co_await lf::child_scope(); + + co_await sc.fork_drop(worker{}); + sc.request_stop(); + co_await sc.join(); +} +``` + +### `token` + +```cpp +auto token() const noexcept -> stop_token; +``` + +Returns a non-owning token referring to this source. Child scopes store tokens +to propagate cancellation. + +### `stop_requested` + +```cpp +auto stop_requested() const noexcept -> bool; +``` + +Returns true if this source or any ancestor source has been stopped. The check +is linear in the parent-chain depth. + +### `request_stop` + +```cpp +auto request_stop() noexcept -> void; +``` + +Requests cancellation for this source and its descendants. Calling it more than +once is allowed. + +### `race_request_stop` + +```cpp +auto race_request_stop() noexcept -> bool; +``` + +Requests cancellation and returns true only for the first caller that changed +the source from not-stopped to stopped. Use `request_stop()` when the return +value is not needed. + +## `stop_source::stop_token` + +```cpp +class stop_source::stop_token { + public: + constexpr stop_token() noexcept; + + auto stop_possible() const noexcept -> bool; + auto stop_requested() const noexcept -> bool; +}; +``` + +`stop_token` is a pointer-sized non-owning handle to a stop-source chain. A +default-constructed token is unstoppable. + +### `stop_possible` + +```cpp +auto stop_possible() const noexcept -> bool; +``` + +Returns true when the token refers to a stop source. + +### `stop_requested` + +```cpp +auto stop_requested() const noexcept -> bool; +``` + +Returns true if any source in the chain has been stopped. A null token always +returns false. + +## Propagation rules + +Normal [`scope()`](scope.md#scope) children inherit the parent's current stop +token. [`child_scope()`](scope.md#child_scope) inserts a new stop source between +the parent token and any children launched by that scope. + +If a child task has not started and its stop token is already stopped, libfork +destroys the child frame without resuming it. At `join`, a cancelled task may +stop resuming the cancelled subtree after required cleanup. diff --git a/docs/api/core/concepts.md b/docs/api/core/concepts.md new file mode 100644 index 000000000..b0b4a1555 --- /dev/null +++ b/docs/api/core/concepts.md @@ -0,0 +1,276 @@ +--- +icon: lucide/puzzle +--- + +# Concepts + +`libfork.core` exposes the concepts used by tasks, schedulers, algorithms, and +custom integrations. They are public API because user-defined schedulers, +contexts, stacks, awaitables, projections, and algorithms are expected to use +the same constraints as libfork itself. + +## Returnable + +```cpp +template +concept returnable = std::is_void_v || (/*plain-object*/ && std::movable); +``` + +Types suitable for use as the `T` in [`task`](task.md). `void` is +allowed. Non-void return types must be movable plain object types. + +### Plain-object + +An exposition-only concept used throughout libfork: + +```cpp +template +concept /*plain-object*/ = + std::is_object_v && + std::same_as>; +``` + +## Worker stacks + +```cpp +template +concept worker_stack = /* ... */; +``` + +`worker_stack` defines the stack API used to allocate coroutine frames. A stack +implementation must provide: + +```cpp +auto push(std::size_t bytes) -> void*; +auto pop(void* ptr, std::size_t bytes) noexcept -> void; +auto checkpoint() noexcept -> std::regular auto; +auto prepare_release() noexcept -> std::movable auto; +auto release(decltype(prepare_release())) noexcept -> void; +auto acquire(decltype(checkpoint()) const&) noexcept -> void; +``` + +The fast path is `push`, `pop`, and `checkpoint`. `prepare_release`, `release`, +and `acquire` are used when continuation stealing transfers stack ownership +between workers. + +Stack checkpoints must be cheap to copy. A default-constructed checkpoint is a +null state that only compares equal to itself. Non-null checkpoints compare +equal when they refer to the same underlying stack allocation source. + +## Worker contexts + +```cpp +template +concept lifo_stack = /* ... */; + +template +concept worker_context = /* ... */; + +template +using stack_t = std::remove_reference_t().stack())>; +``` + +`lifo_stack` requires: + +```cpp +context.push(value); // -> void +context.pop(); // noexcept -> U +``` + +`worker_context` refines that shape for `steal_handle` and also requires +access to a worker stack: + +```cpp +auto stack() noexcept -> Stack&; // Stack models worker_stack +``` + +Contexts are the per-worker execution state. They store stealable +continuations, expose the coroutine frame stack, and are temporarily bound to a +thread while `execute` resumes a task. + +## Schedulers + +```cpp +template +concept has_context_typedef = requires { + typename std::remove_cvref_t::context_type; +}; + +template +using context_t = typename std::remove_cvref_t::context_type; + +template +concept scheduler = /* ... */; +``` + +A scheduler provides a `context_type` and can post a scheduled root task: + +```cpp +void post(lf::sched_handle); +``` + +`post` must provide the strong exception guarantee. If it returns normally, the +task associated with the handle must eventually be resumed by a worker using +[`execute`](scheduling.md#execute). + +## Async invocables + +```cpp +template +concept async_invocable = /* ... */; + +template +concept async_regular_invocable = async_invocable; + +template +concept async_nothrow_invocable = /* ... */; + +template +using async_result_t = /* task value_type */; + +template +concept async_invocable_to = /* ... */; + +template +concept async_nothrow_invocable_to = /* ... */; +``` + +`async_invocable` checks whether `Fn` can be called as a libfork task for the +given `Context` and arguments. The result must be `task` for some +`R`. + +When overload resolution is checked, libfork first tries to call: + +```cpp +fn(lf::env{}, args...); +``` + +If that is not viable, it checks: + +```cpp +fn(args...); +``` + +This is what makes [`env`](env.md) useful for context-generic tasks. +`async_result_t` is the `value_type` of the returned task. + +## Awaitables + +```cpp +template +constexpr auto acquire_awaitable(T&& t); + +template +concept awaitable = /* ... */; +``` + +`acquire_awaitable` implements the same acquisition rule used by `co_await`: + +- if `t.operator co_await()` is available, use it; +- otherwise, if `operator co_await(t)` is available, use it; +- otherwise, treat `t` itself as the awaitable. + +`awaitable` is libfork's context-switching awaitable concept. The +acquired awaitable must be storable and provide: + +```cpp +auto await_ready() -> bool-convertible; +auto await_suspend(lf::sched_handle, Context&) -> void; +auto await_resume(); +``` + +!!! warning + `await_suspend` must not complete the same coroutine inline. A custom + awaitable hands the suspended task to another scheduling path, which must + later resume it with `execute`. + +## Indirect invocables + +```cpp +namespace sync { + template + concept indirectly_unary_invocable = /* ... */; + + template + concept indirectly_regular_unary_invocable = /* ... */; +} + +namespace async { + template + concept indirectly_unary_invocable = /* ... */; + + template + concept indirectly_regular_unary_invocable = /* ... */; +} + +template +concept indirectly_unary_invocable = + sync::indirectly_unary_invocable || + async::indirectly_unary_invocable; + +template +concept indirectly_regular_unary_invocable = + sync::indirectly_regular_unary_invocable || + async::indirectly_regular_unary_invocable; +``` + +These concepts mirror the standard indirect callable concepts, but also support +libfork's async projections and the `indirect_value_t` customization used by +[`projected`](projected.md). + +The combined concepts accept either a synchronous callable or an asynchronous +callable. If both are viable, libfork algorithms generally prefer the async +form. + +## Semigroups + +```cpp +namespace sync { + template + concept indirect_semigroup = /* ... */; +} + +namespace async { + template + concept indirect_semigroup = /* ... */; +} + +template +concept indirect_semigroup = + sync::indirect_semigroup || + async::indirect_semigroup; + +template +concept indirect_commutative_semigroup = indirect_semigroup; + +template +using indirect_semigroup_t = /* operation result type */; +``` + +An indirect semigroup is an indirectly readable input plus an associative binary +operation that is closed over all combinations of: + +- the projected indirect value type; +- the iterator reference type; +- the operation result type. + +The async variant requires those combinations to be valid libfork task +invocations. `indirect_commutative_semigroup` is a semantic refinement: the +type system cannot prove commutativity, so users are responsible for supplying +an operation where `a op b == b op a`. + +`indirect_semigroup_t` returns the result type of applying the operation to two +elements. + +## Projections + +```cpp +template +concept projectable = /* ... */; + +template Fn> +using projected = /* ... */; +``` + +See [Projections](projected.md) for the public projection alias and the extra +default-initialization rule for async projections. diff --git a/docs/api/core/context.md b/docs/api/core/context.md new file mode 100644 index 000000000..09daefa6f --- /dev/null +++ b/docs/api/core/context.md @@ -0,0 +1,83 @@ +--- +icon: lucide/cpu +--- + +# Contexts + +Contexts are the worker-local objects used by `execute`. They own a coroutine +frame stack and a LIFO collection of stealable continuations. + +The core module exposes reusable context base classes. Concrete contexts are in +`libfork.batteries` and `libfork.schedulers`, but custom schedulers can build on +these types directly. + +## `base_context` + +```cpp +template +class base_context { + public: + auto stack() noexcept -> Stack&; + + protected: + constexpr base_context(); + + template + requires std::constructible_from + explicit constexpr base_context(Args&&... args); +}; +``` + +`base_context` stores a worker stack and exposes it through `stack()`. It is a +convenience base for context implementations that want stack storage without +committing to a particular queue or scheduler policy. + +```cpp +class my_context : public lf::base_context { + public: + using base_context::base_context; + + void push(lf::steal_handle); + auto pop() noexcept -> lf::steal_handle; +}; +``` + +`base_context` does not itself model +[`worker_context`](concepts.md#worker-contexts); derived types must add +`push` and `pop`. + +## `poly_context` + +```cpp +template +class poly_context : public base_context { + public: + using base_context::base_context; + + virtual void push(steal_handle) = 0; + virtual auto pop() noexcept -> steal_handle = 0; + virtual void post(sched_handle handle); + + virtual ~poly_context() noexcept = default; +}; +``` + +`poly_context` is the standard polymorphic context base. It is polymorphic over +`push`, `pop`, and optionally `post`, while the stack type remains part of the +static type. + +The default `post` throws [`post_error`](#post_error). Scheduler-like derived +contexts override it when they can accept externally scheduled root work. + +Use `poly_context` when a scheduler or adapter needs dynamic dispatch but still +wants libfork's typed handle discipline. + +## `post_error` + +```cpp +struct post_error final : libfork_exception { + auto what() const noexcept -> const char* override; +}; +``` + +Thrown by `poly_context::post` when a derived context does not override posting. diff --git a/docs/api/core/env.md b/docs/api/core/env.md new file mode 100644 index 000000000..3307df7f9 --- /dev/null +++ b/docs/api/core/env.md @@ -0,0 +1,45 @@ +--- +icon: lucide/earth +--- + +# Env + +```cpp +template +struct env {} +``` + +See associated: + +- [worker_context](./concepts.md#worker-contexts) + +A tag type that can be used to help write context-generic code. This can be +used as the (templated) first parameter of a coroutine such that +[worker_context](./concepts.md#worker-contexts) can be deduced. This parameter +will be generated and passed to the coroutine by libfork automatically. + +!!! note + This type is not user-constructible. + +!!! example + For example: + ```cpp + struct context_generic { + template + auto operator()(lf::env, int param) -> lf::task { + // ... + } + }; + ``` + Then this can be called from any context: + ```cpp linenums="1" + auto some_coro(/* args */) -> lf::task { + // ... + co_await lf::invoke(context_generic{}, 42); + // ... + } + ``` + !!! note + Here we defined `context_generic` as a function object so that we could + pass it to [lf::invoke](./invoke.md) without needing to specify template + parameters. diff --git a/docs/api/core/exceptions.md b/docs/api/core/exceptions.md new file mode 100644 index 000000000..a4397c8da --- /dev/null +++ b/docs/api/core/exceptions.md @@ -0,0 +1,30 @@ +--- +icon: lucide/triangle-alert +--- + +# Exceptions + +Core exceptions derive from `libfork_exception`. + +## `libfork_exception` + +```cpp +struct libfork_exception : std::exception {}; +``` + +The base class for exceptions thrown by `libfork.core`. + +## Derived exceptions + +| Exception | Thrown by | +| --- | --- | +| `schedule_error` | `schedule` called from a worker thread already bound to the same context type | +| `execute_error` | `execute` called recursively on a thread already bound to the same context type | +| `steal_overflow_error` | a single task exceeds libfork's internal steal counter | +| `root_alloc_error` | a root coroutine frame exceeds the receiver state's embedded buffer | +| `broken_receiver_error` | receiver operations on an invalid receiver | +| `operation_cancelled_error` | `receiver::get()` after cancellation was requested | +| `post_error` | default `poly_context::post` implementation | + +See the pages for [scheduling](scheduling.md), [receivers](receiver.md), and +[contexts](context.md) for the exact operation-level behavior. diff --git a/docs/api/core/handles.md b/docs/api/core/handles.md new file mode 100644 index 000000000..35123557f --- /dev/null +++ b/docs/api/core/handles.md @@ -0,0 +1,86 @@ +--- +icon: lucide/key-round +--- + +# Handles + +Handles are lightweight wrappers around libfork coroutine frames. They are used +by context and scheduler implementations, not by ordinary task code. + +All handle types are nullable and testable with explicit `operator bool`. + +## `unsafe_steal_handle` + +```cpp +struct unsafe_steal_handle { + constexpr unsafe_steal_handle() = default; + explicit constexpr operator bool() const noexcept; + auto operator==(unsafe_steal_handle const&) const noexcept -> bool = default; +}; +``` + +An untyped handle to a stealable continuation. It exists for erased storage +policies such as generic deques. + +Prefer [`steal_handle`](#steal_handle) whenever the context type is known. + +## `unsafe_sched_handle` + +```cpp +struct unsafe_sched_handle { + constexpr unsafe_sched_handle() = default; + explicit constexpr operator bool() const noexcept; + auto operator==(unsafe_sched_handle const&) const noexcept -> bool = default; +}; +``` + +An untyped handle to scheduled work. It exists for erased storage policies. + +Prefer [`sched_handle`](#sched_handle) whenever the context type is known. + +## `steal_handle` + +```cpp +template +struct steal_handle : unsafe_steal_handle { + using unsafe_steal_handle::unsafe_steal_handle; +}; +``` + +A typed handle to a continuation that may be resumed with: + +```cpp +lf::execute(context, handle); +``` + +The coroutine behind a `steal_handle` is suspended at a fork point. Contexts +store these handles in LIFO order and thieves may take them through a stealing +policy. + +## `sched_handle` + +```cpp +template +struct sched_handle : unsafe_sched_handle { + using unsafe_sched_handle::unsafe_sched_handle; +}; +``` + +A typed handle to scheduled work. The coroutine behind a `sched_handle` is +either a not-yet-started root task or a task suspended at a custom +context-switching awaitable. + +Schedulers receive `sched_handle` in `post`, store it as needed, +and eventually resume it with `execute`. + +## Safety + +Handles do not own coroutine frames. They are only valid under the protocol that +created them: + +- `sched_handle` must eventually be resumed by a scheduler compatible with + `T`; +- `steal_handle` must be treated as consumed once a worker starts executing + it; +- untyped handles should only be used inside storage adapters that restore the + correct typed handle before execution. diff --git a/docs/api/core/index.md b/docs/api/core/index.md new file mode 100644 index 000000000..629a65e2d --- /dev/null +++ b/docs/api/core/index.md @@ -0,0 +1,54 @@ +--- +icon: lucide/blocks +--- + +# Core + +All public symbols documented here live in namespace `lf` and are reachable via: + +```cpp +import libfork.core; +``` + +`libfork.core` is the minimal public module for writing libfork tasks and for +building schedulers, contexts, stacks, and higher-level algorithms. It does not +include concrete schedulers or stack implementations; those are provided by +`libfork.batteries` and `libfork.schedulers`. + +Use this module directly when you want the core coroutine protocol without the +standard batteries. Most applications can instead import the meta-module: + +```cpp +import libfork; +``` + +## What core provides + +- [Tasks](task.md): `task` and `env`. +- [Scopes](scope.md): `scope()`, `child_scope()`, `fork`, `call`, and `join`. +- [Scheduling](scheduling.md): `schedule`, `execute`, and root-task errors. +- [Receivers](receiver.md): `recv_state`, `receiver`, waiting, result retrieval, + and root cancellation. +- [Cancellation](cancellation.md): `stop_source` and `stop_token`. +- [Contexts](context.md): `base_context`, `poly_context`, and `post_error`. +- [Handles](handles.md): typed and untyped task handles. +- [Projections](projected.md): async-aware `projectable` and `projected`. +- [Concepts](concepts.md): the constraints used by the public API. +- [Exceptions](exceptions.md): the common exception hierarchy. + +## Execution model + +Core tasks are coroutines arranged into a strict fork-join tree. A task may +create child tasks with `fork` or `call`, but those children are always joined +before the parent can return. Libfork uses continuation stealing: a forked child +runs immediately, while the parent's continuation becomes stealable work. + +The core module deliberately separates three roles: + +- a **task** is a coroutine with a `task` return type; +- a **context** owns the worker-local stack and a LIFO queue of stealable + continuations; +- a **scheduler** accepts root work and eventually resumes it with `execute`. + +That split is what lets `libfork.core` stay independent from any specific +thread pool or work-stealing queue. diff --git a/docs/api/core/invoke.md b/docs/api/core/invoke.md new file mode 100644 index 000000000..4153dd373 --- /dev/null +++ b/docs/api/core/invoke.md @@ -0,0 +1,7 @@ +--- +icon: lucide/circle-play +--- + +# Invoke + +TODO: waiting on implementation diff --git a/docs/api/core/projected.md b/docs/api/core/projected.md new file mode 100644 index 000000000..77531e0db --- /dev/null +++ b/docs/api/core/projected.md @@ -0,0 +1,58 @@ +--- +icon: lucide/filter +--- + +# Projections + +Libfork algorithms support projections that may be synchronous or asynchronous. +The core module exposes the concepts and aliases used to model those projected +iterator values. + +## `projectable` + +```cpp +template +concept projectable = /* ... */; +``` + +`projectable` checks that `Fn` can be used as a projection for +the indirectly readable type `I`. + +Synchronous projections follow the usual standard-library shape. Async +projections are libfork tasks and may receive `env`: + +```cpp +struct square_async { + template + auto operator()(lf::env, int x) const -> lf::task { + co_return x * x; + } +}; +``` + +Async projections must also produce default-initializable result types. Libfork +algorithms need scratch storage for async projected values before child tasks +write their results. + +## `projected` + +```cpp +template Fn> +using projected = /* exposition-only projected iterator type */; +``` + +`projected` is libfork's async-aware analogue of `std::projected`. It provides +the associated value and reference types that indirect concepts use when a range +is viewed through a projection. + +For synchronous callables, `projected` behaves like the standard projection +machinery. For async callables, its value and reference types come from +[`async_result_t`](concepts.md#async-invocables). + +```cpp +using P = lf::projected::iterator, square_async>; +``` + +Most users do not name `projected` directly. It is primarily useful when +writing algorithms that should accept the same sync and async projection forms +as libfork's built-in algorithms. diff --git a/docs/api/core/receiver.md b/docs/api/core/receiver.md new file mode 100644 index 000000000..a7d8e0400 --- /dev/null +++ b/docs/api/core/receiver.md @@ -0,0 +1,167 @@ +--- +icon: lucide/inbox +--- + +# Receivers + +Receivers are the completion handles returned by +[`schedule`](scheduling.md#schedule). They are intentionally separate from +tasks: a `task` belongs to the coroutine tree, while a `receiver` belongs to +the outside caller waiting for a root task. + +## `recv_state` + +```cpp +template +class recv_state { + public: + recv_state(); + + template + requires std::constructible_from + explicit recv_state(Args&&... args); + + template + recv_state(std::allocator_arg_t, Alloc const& alloc); + + template + requires std::constructible_from + recv_state(std::allocator_arg_t, Alloc const& alloc, Args&&... args); + + recv_state(recv_state&&) noexcept; + auto operator=(recv_state&&) noexcept -> recv_state&; + + recv_state(recv_state const&) = delete; + auto operator=(recv_state const&) -> recv_state& = delete; +}; +``` + +`recv_state` owns the shared state used by a scheduled root task and its +receiver. It contains the result storage, exception storage, ready flag, optional +root stop source, and the embedded root-frame buffer. + +Most users can rely on the convenience `schedule` overload, which creates a +non-stoppable state automatically: + +```cpp +auto recv = lf::schedule(pool, root_task{}); +``` + +Construct `recv_state` yourself when you need stoppability, allocator-aware +state allocation, or a non-default initial result value: + +```cpp +lf::recv_state state{0}; +auto recv = lf::schedule(pool, std::move(state), root_task{}); +``` + +`recv_state` is move-only and is consumed by `schedule`. + +## `receiver` + +```cpp +template +class receiver { + public: + receiver(receiver&&) noexcept; + auto operator=(receiver&&) noexcept -> receiver&; + + receiver(receiver const&) = delete; + auto operator=(receiver const&) -> receiver& = delete; + + auto valid() const noexcept -> bool; + auto ready() const -> bool; + void wait() const; + + auto stop_source() -> stop_source& requires Stoppable; + + auto get() && -> T; +}; +``` + +`receiver` is a move-only handle to scheduled root-task +completion. + +### `valid` + +```cpp +auto valid() const noexcept -> bool; +``` + +Returns whether this receiver still refers to shared state. A receiver becomes +invalid after `std::move(receiver).get()`. + +### `ready` + +```cpp +auto ready() const -> bool; +``` + +Returns whether the root task has completed, either with a value, with an +exception, or through cancellation. Throws `broken_receiver_error` if the +receiver is invalid. + +### `wait` + +```cpp +void wait() const; +``` + +Blocks until the root task completes. `wait` may be called multiple times. +Throws `broken_receiver_error` if the receiver is invalid. + +### `stop_source` + +```cpp +auto stop_source() -> lf::stop_source& requires Stoppable; +``` + +Returns the root stop source for stoppable receivers. Requesting stop prevents +not-yet-started cancellable work from running and causes cancellation-aware join +paths to stop resuming cancelled subtrees. + +```cpp +lf::recv_state state; +auto recv = lf::schedule(pool, std::move(state), root_task{}); +recv.stop_source().request_stop(); +``` + +### `get` + +```cpp +auto get() && -> T; +``` + +Waits for completion, consumes the receiver state, and returns the result. For +`T = void`, it returns nothing. + +If the task completed with an exception, `get` rethrows it. If `Stoppable` is +`true` and the receiver's stop source has been requested, `get` throws +`operation_cancelled_error`. + +!!! warning + `get` is rvalue-qualified and may only be called once: + ```cpp + auto value = std::move(recv).get(); + ``` + +## `broken_receiver_error` + +```cpp +struct broken_receiver_error final : libfork_exception { + auto what() const noexcept -> const char* override; +}; +``` + +Thrown by `ready`, `wait`, or `stop_source` when called on an invalid receiver. + +## `operation_cancelled_error` + +```cpp +struct operation_cancelled_error final : libfork_exception { + auto what() const noexcept -> const char* override; +}; +``` + +Thrown by `std::move(receiver).get()` for a stoppable receiver whose root stop +source has been requested. diff --git a/docs/api/core/scheduling.md b/docs/api/core/scheduling.md new file mode 100644 index 000000000..ed5fe753f --- /dev/null +++ b/docs/api/core/scheduling.md @@ -0,0 +1,135 @@ +--- +icon: lucide/calendar-clock +--- + +# Scheduling + +Scheduling bridges outside code and libfork tasks. `schedule` creates a root +task and submits it to a scheduler; `execute` resumes a task handle on a worker +context. + +## `schedule` + +```cpp +template + requires async_invocable_to, R, context_t, std::decay_t...> +[[nodiscard]] +constexpr auto schedule( + Sch&& sch, + recv_state state, + Fn&& fn, + Args&&... args) -> receiver; +``` + +Schedules a root task using caller-provided receiver state. `Fn` and `Args...` +are decayed into the root coroutine frame. The returned +[`receiver`](receiver.md#receiver) observes completion, exceptions, and the +result. + +Use this overload when you need a custom allocator for receiver state, an +initial return object value, or a stoppable root task: + +```cpp +lf::recv_state state; +auto recv = lf::schedule(pool, std::move(state), root_task{}, 42); + +recv.stop_source().request_stop(); +int result = std::move(recv).get(); +``` + +!!! warning + `schedule` must not be called from inside a worker thread that is already + executing the same context type. Doing so throws `schedule_error`. + +### Convenience overload + +```cpp +template + requires /* async invocable with default-schedulable result */ +[[nodiscard]] +constexpr auto schedule(Sch&& sch, Fn&& fn, Args&&... args) + -> receiver, context_t, std::decay_t...>>; +``` + +This overload creates a non-stoppable `recv_state` with the default allocator. +The task result must be `void` or default-initializable and movable. + +```cpp +auto recv = lf::schedule(pool, root_task{}, 42); +auto value = std::move(recv).get(); +``` + +## `execute` + +```cpp +template +constexpr void execute(Context& context, sched_handle handle); + +template +constexpr void execute(Context& context, steal_handle handle); +``` + +Binds the current thread to `context`, resumes the task represented by +`handle`, and unbinds the thread before returning. + +Scheduler implementations call `execute` after taking a handle from their work +source: + +```cpp +if (auto h = queue.pop()) { + lf::execute(context, h); +} +``` + +The `sched_handle` overload resumes root tasks and tasks suspended by custom +awaitables. The `steal_handle` overload resumes a stolen continuation and marks +the frame as stolen before execution. + +!!! warning + `execute` must not be called recursively on a thread already bound to a + context of the same type. Doing so throws `execute_error`. + +## `schedule_error` + +```cpp +struct schedule_error final : libfork_exception { + auto what() const noexcept -> const char* override; +}; +``` + +Thrown when `schedule` is called from inside a worker thread for the same +context type. + +## `execute_error` + +```cpp +struct execute_error final : libfork_exception { + auto what() const noexcept -> const char* override; +}; +``` + +Thrown when `execute` is called while the current thread is already executing a +task for the same context type. + +## `steal_overflow_error` + +```cpp +struct steal_overflow_error final : libfork_exception { + auto what() const noexcept -> const char* override; +}; +``` + +Thrown if a single task is stolen enough times to overflow libfork's internal +steal counter. + +## `root_alloc_error` + +```cpp +struct root_alloc_error final : libfork_exception { + auto what() const noexcept -> const char* override; +}; +``` + +Thrown when the root coroutine frame does not fit into the buffer embedded in +the receiver state. This usually means the scheduled callable or its arguments +are too large to store directly in the root frame. diff --git a/docs/api/core/scope.md b/docs/api/core/scope.md new file mode 100644 index 000000000..2f1cc6a78 --- /dev/null +++ b/docs/api/core/scope.md @@ -0,0 +1,166 @@ +--- +icon: lucide/git-branch +--- + +# Scopes + +```cpp +[[nodiscard]] +constexpr auto scope() noexcept; + +[[nodiscard]] +constexpr auto child_scope() noexcept; +``` + +Scopes are acquired inside a libfork task: + +```cpp +auto sc = co_await lf::scope(); +``` + +The returned scope object is the public way to create children and join them. +The exact scope type is intentionally unnamed, but its member functions are part +of the API. + +## `scope` + +`scope()` creates a normal child-launching scope. It inherits the current task's +stop token. + +```cpp +auto sc = co_await lf::scope(); +``` + +The scope object is immovable and should be used locally. It exposes `fork`, +`fork_drop`, `call`, `call_drop`, and `join`. + +## `child_scope` + +`child_scope()` creates a normal scope plus a new embedded +[`stop_source`](cancellation.md#stop_source). Children launched from the scope +receive that stop source's token, chained to the parent's token. + +```cpp +auto sc = co_await lf::child_scope(); +sc.request_stop(); +co_await sc.join(); +``` + +Because the returned object derives from `stop_source`, it also exposes: + +```cpp +auto token() const noexcept -> stop_source::stop_token; +auto stop_requested() const noexcept -> bool; +auto request_stop() noexcept -> void; +auto race_request_stop() noexcept -> bool; +``` + +## `fork` + +```cpp +template Fn> +auto fork(R* ret, Fn&& fn, Args&&... args) noexcept; + +template Fn> +auto fork(Fn&& fn, Args&&... args) noexcept; +``` + +`fork` creates a child task and makes the parent continuation stealable. The +child starts running immediately on the current worker. The parent continues +later, either on this worker or on a worker that stole the continuation. + +Use the pointer overload to receive a non-void result: + +```cpp +template +auto parent(lf::env) -> lf::task { + int value = 0; + auto sc = co_await lf::scope(); + + co_await sc.fork(&value, child{}); + co_await sc.join(); + + co_return value; +} +``` + +The pointed-to object must remain alive until `join` completes. That is +normally achieved by using a local variable in the parent task. + +## `fork_drop` + +```cpp +template Fn> +auto fork_drop(Fn&& fn, Args&&... args) noexcept; +``` + +`fork_drop` launches a child and discards its result. It accepts both `void` and +non-void child tasks. + +```cpp +co_await sc.fork_drop(write_log{}, item); +``` + +## `call` + +```cpp +template Fn> +auto call(R* ret, Fn&& fn, Args&&... args) noexcept; + +template Fn> +auto call(Fn&& fn, Args&&... args) noexcept; +``` + +`call` invokes a child task as a direct child but does not make the parent +continuation stealable. It behaves like an async function call in the fork-join +tree. + +Use it when the parent cannot profitably continue in parallel with the child: + +```cpp +int value = 0; +co_await sc.call(&value, child{}, input); +``` + +## `call_drop` + +```cpp +template Fn> +auto call_drop(Fn&& fn, Args&&... args) noexcept; +``` + +`call_drop` is the direct-call equivalent of `fork_drop`. + +## `join` + +```cpp +auto join() noexcept; +``` + +`join` waits for all forked children in the current scope to complete. A task +that forks children must join before returning. + +```cpp +co_await sc.join(); +``` + +If no child continuation was stolen, `join` is a fast local operation. If one or +more continuations were stolen, `join` participates in the join race and may +suspend the current coroutine until the last child completes. + +## Exceptions + +Exceptions thrown by children are stashed in the parent and rethrown from +`join`. If several children throw, libfork preserves one exception. + +If a task is already cancelled when a child would start, the child frame is +destroyed without running. Exceptions already recorded in a cancelled subtree +may be dropped while cancellation unwinds that subtree. + +## Choosing `fork` or `call` + +Use `fork` when the parent has useful work that can run in parallel with the +child or when multiple independent children should race toward a later `join`. + +Use `call` when the parent needs the result immediately and exposing the parent +continuation as stealable work would only add scheduler traffic. diff --git a/docs/api/core/task.md b/docs/api/core/task.md new file mode 100644 index 000000000..2e5e5e90d --- /dev/null +++ b/docs/api/core/task.md @@ -0,0 +1,45 @@ +--- +icon: lucide/box +--- + +# Task + +```cpp +template +class task { + public: + using value_type = T; + using context_type = Context; +}; +``` + +See associated: + +- [returnable](./concepts.md#returnable) +- [worker_context](./concepts.md#worker-contexts) + +The return type for all coroutines/async-functions in libfork. This type exists +so that users can mark their functions as coroutines. Other than its typedefs +it has no public interface. + +!!! warning + No consumer of this library should ever touch an instance of this type, + it is used for specifying the return type of a coroutine only. + +!!! example + With a simple type alias, coroutines can be written more compactly: + + ```cpp + template + using task = lf::task; + ``` + + Now you can write coroutines like this: + + ```cpp + auto my_coroutine() -> task { + co_return 42; + } + ``` + + See [env](env.md) for writing context-generic coroutines. diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 000000000..9c6dc013c --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,27 @@ +--- +icon: lucide/book-open +--- + +# API reference + +All public symbols documented here live in namespace `lf` and are reachable via: + +```cpp +import libfork; +``` + +The meta-module re-exports the following modules: + +- `libfork.core` +- `libfork.batteries` +- `libfork.schedulers` +- `libfork.algorithm` + +Each of these modules is documented in its own section: + +- [Core](core/index.md): Central component. +- [Batteries](batteries.md): worker stacks, deque, context policies, and context implementations. +- [Schedulers](schedulers.md): inline and busy-pool schedulers. +- [Algorithm](algorithm.md): fork-join algorithms over random-access ranges. + +`libfork.utils` is a support module and is not documented as user-facing API. diff --git a/docs/api/schedulers.md b/docs/api/schedulers.md new file mode 100644 index 000000000..bfc098844 --- /dev/null +++ b/docs/api/schedulers.md @@ -0,0 +1,111 @@ +--- +icon: lucide/network +--- + +# Schedulers + +Schedulers map libfork task handles to worker contexts and OS threads. User +task code is independent of the scheduler as long as it satisfies the +`scheduler` concept. + +## Inline scheduler + +### `concept derived_worker_context` + +`Context` has a `context_type` alias and derives from that context type. This is +the shape required by `inline_scheduler`. + +### `inline_scheduler` + +Single-threaded scheduler that owns one context. `post` immediately calls +`execute` on the calling thread. + +Constructor forms: + +- `inline_scheduler()` +- `explicit inline_scheduler(Args&&...)`, forwarded to `Context` + +Public API: + +- `using context_type = Context::context_type` +- `post(sched_handle) -> void` + +Inline schedulers are useful for tests, debugging, and measuring stack/context +overhead without worker-thread scheduling. + +### `mono_inline_scheduler` + +Alias for: + +```cpp +lf::inline_scheduler> +``` + +### `poly_inline_scheduler` + +Alias for: + +```cpp +lf::inline_scheduler> +``` + +## Busy pool + +### `enum class pool_kind` + +Selects the context implementation used by `basic_busy_pool`. + +- `pool_kind::mono` +- `pool_kind::poly` + +### `basic_busy_pool, Alloc = std::allocator>` + +Busy-waiting work-stealing thread pool. It creates `n` `std::jthread` workers, +posts root tasks into a shared queue, and lets idle workers steal from other +worker contexts. + +Constructor: + +```cpp +explicit basic_busy_pool( + std::size_t n = std::thread::hardware_concurrency(), + Alloc const& alloc = Alloc()); +``` + +Public API: + +- `using context_type` +- `post(sched_handle) -> void` + +The pool is non-copyable and non-movable. Destruction requests stop on all +workers and joins them through `std::jthread`. + +!!! note + + `basic_busy_pool` currently busy-waits for work. It is useful for benchmark + and development scenarios where low latency matters more than idle power. + +### `mono_busy_pool, Alloc = std::allocator>` + +Alias for: + +```cpp +lf::basic_busy_pool +``` + +This is the usual pool shape for concrete contexts: + +```cpp +lf::mono_busy_pool> pool{4}; +``` + +### `poly_busy_pool, Alloc = std::allocator>` + +Alias for: + +```cpp +lf::basic_busy_pool +``` + +Use the polymorphic variant when scheduler/context code needs the +`poly_context` abstraction. diff --git a/docs/benchmarks.md b/docs/benchmarks.md new file mode 100644 index 000000000..0cd83b31c --- /dev/null +++ b/docs/benchmarks.md @@ -0,0 +1,36 @@ +--- +icon: lucide/activity +--- + +# Benchmarks + +The benchmark suite is present in the repository but this docs section is a +stub for published results. + +Current benchmark groups include: + +- recursive Fibonacci task overhead; +- `fold` over memory-backed and lazy ranges; +- unbalanced tree search (UTS); +- explicit scheduler switching between pools; +- serial, OpenMP, bare-metal, and libfork implementations where available. + +Build benchmarks in release mode: + +```sh +cmake --preset ci-release -DCMAKE_TOOLCHAIN_FILE=cmake/llvm-brew-toolchain.cmake +cmake --build --preset ci-release +``` + +On Linux, use `cmake/gcc-brew-toolchain.cmake`. + +Benchmark code is organized under: + +- `benchmark/lib/` for shared benchmark utilities; +- `benchmark/src/libfork/` for libfork implementations; +- `benchmark/src/serial/`, `benchmark/src/openmp/`, and other implementation + directories for comparisons; +- `benchmark/external/` for bundled benchmark inputs such as UTS. + +Future versions of this page should include reproducible machine details, +compiler versions, command lines, raw result artifacts, and plots. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 000000000..4b7e96e54 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,86 @@ +--- +icon: lucide/git-pull-request +--- + +# Contributing + +This repository currently targets C++26, CMake module file sets, and C++23 +`import std`. Build configuration must use the platform toolchain file. + +## Dependencies + +Install development dependencies with Homebrew. + +macOS: + +```sh +brew install cmake ninja catch2 google-benchmark clang-format codespell llvm +``` + +Linux: + +```sh +brew install cmake ninja catch2 google-benchmark clang-format codespell gcc binutils +``` + +## Configure, build, test + +Use `ci-hardened` for normal development: + +```sh +cmake --preset ci-hardened -DCMAKE_TOOLCHAIN_FILE=cmake/llvm-brew-toolchain.cmake +cmake --build --preset ci-hardened +ctest --preset ci-hardened +``` + +On Linux, use: + +```sh +cmake --preset ci-hardened -DCMAKE_TOOLCHAIN_FILE=cmake/gcc-brew-toolchain.cmake +``` + +Use `ci-release` for benchmarks: + +```sh +cmake --preset ci-release -DCMAKE_TOOLCHAIN_FILE=cmake/llvm-brew-toolchain.cmake +cmake --build --preset ci-release +``` + +Expected warnings include CMake's experimental `import std` warning and the +benchmark warning about release mode when building benchmarks through +`ci-hardened`. + +## Documentation + +The docs site is built with zensical. With the Python project environment: + +```sh +uv sync --group dev +uv run zensical serve +uv run zensical build --clean +``` + +Without `uv`, install zensical directly: + +```sh +python -m pip install zensical +zensical serve +zensical build --clean +``` + +The configured output directory is `build/site`. + +## Source changes + +Module files live under `src/`. If a source or public header file is added or +removed, update the root `CMakeLists.txt` module/header file sets. Tests live in +`test/src/` and are discovered recursively by CMake. + +For API changes, update the matching docs page under `docs/api/` and add or +adjust tests that exercise the behavior. + +## Benchmarks + +Benchmark sources live under `benchmark/`. Use the release preset before +comparing timings. The benchmark target depends on Google Benchmark and is +enabled when `libfork_DEV_MODE=ON`, which is set by the CI presets. diff --git a/docs/favicon/android-chrome-192x192.png b/docs/favicon/android-chrome-192x192.png new file mode 100644 index 000000000..fbcb1b81d Binary files /dev/null and b/docs/favicon/android-chrome-192x192.png differ diff --git a/docs/favicon/android-chrome-512x512.png b/docs/favicon/android-chrome-512x512.png new file mode 100644 index 000000000..43037a571 Binary files /dev/null and b/docs/favicon/android-chrome-512x512.png differ diff --git a/docs/favicon/apple-touch-icon.png b/docs/favicon/apple-touch-icon.png new file mode 100644 index 000000000..b2cdc1b3a Binary files /dev/null and b/docs/favicon/apple-touch-icon.png differ diff --git a/docs/favicon/favicon-16x16.png b/docs/favicon/favicon-16x16.png new file mode 100644 index 000000000..1ba8d0c14 Binary files /dev/null and b/docs/favicon/favicon-16x16.png differ diff --git a/docs/favicon/favicon-32x32.png b/docs/favicon/favicon-32x32.png new file mode 100644 index 000000000..b282eff9d Binary files /dev/null and b/docs/favicon/favicon-32x32.png differ diff --git a/docs/favicon/favicon.ico b/docs/favicon/favicon.ico new file mode 100644 index 000000000..06e59e7a9 Binary files /dev/null and b/docs/favicon/favicon.ico differ diff --git a/.legacy/docs/_static/site.webmanifest b/docs/favicon/site.webmanifest similarity index 100% rename from .legacy/docs/_static/site.webmanifest rename to docs/favicon/site.webmanifest diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 000000000..c924381b2 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,133 @@ +--- +icon: lucide/play +--- + +# Getting started + +`libfork` is a C++26 module-based library. The current tree uses CMake module +file sets and C++23 `import std`, so the compiler and CMake invocation matter. + +## Requirements + +Use Homebrew-provided build tools and the repository toolchain file. + +On macOS: + +```sh +brew install cmake ninja catch2 google-benchmark clang-format codespell llvm +``` + +On Linux: + +```sh +brew install cmake ninja catch2 google-benchmark clang-format codespell gcc binutils +``` + +The required configure command differs by platform: + +```sh +# macOS +cmake --preset ci-hardened -DCMAKE_TOOLCHAIN_FILE=cmake/llvm-brew-toolchain.cmake + +# Linux +cmake --preset ci-hardened -DCMAKE_TOOLCHAIN_FILE=cmake/gcc-brew-toolchain.cmake +``` + +!!! warning + + Always pass the toolchain file. Without it, CMake may fail to discover + `import std` support or the standard library module metadata. + +## Build and test + +For normal development use the hardened preset: + +```sh +cmake --preset ci-hardened -DCMAKE_TOOLCHAIN_FILE=cmake/llvm-brew-toolchain.cmake +cmake --build --preset ci-hardened +ctest --preset ci-hardened +``` + +For benchmark builds use the release preset: + +```sh +cmake --preset ci-release -DCMAKE_TOOLCHAIN_FILE=cmake/llvm-brew-toolchain.cmake +cmake --build --preset ci-release +``` + +On Linux, replace `cmake/llvm-brew-toolchain.cmake` with +`cmake/gcc-brew-toolchain.cmake`. + +## First task + +A libfork async function is a function object that returns `lf::task`. +The first argument is usually `lf::env`. + +```cpp +import std; +import libfork; + +struct answer { + template + static auto operator()(lf::env) -> lf::task { + co_return 42; + } +}; + +auto main() -> int { + lf::mono_busy_pool> pool{2}; + int value = lf::schedule(pool, answer{}).get(); + return value == 42 ? 0 : 1; +} +``` + +`schedule` returns an `lf::receiver`. Call `.get()` on the receiver to wait +for completion, consume the receiver, return the task result, or rethrow an +exception stored by the task. + +## First fork-join scope + +Use `lf::scope()` inside a task when it needs children: + +```cpp +struct sum_pair { + template + static auto operator()(lf::env, int a, int b) -> lf::task { + co_return a + b; + } +}; + +struct parent { + template + static auto operator()(lf::env) -> lf::task { + int left = 0; + int right = 0; + + auto sc = co_await lf::scope(); + co_await sc.fork(&left, sum_pair{}, 1, 2); + co_await sc.call(&right, sum_pair{}, 3, 4); + co_await sc.join(); + + co_return left + right; + } +}; +``` + +Use `fork` for work that may run in parallel with the continuation. Use `call` +when running the child inline is the better fit. Results written through return +addresses are safe to read only after the matching `join`. + +## Consuming from another project + +The current project is module-based. A consuming CMake project should link the +library target and compile with a compiler/toolchain that supports C++ module +file sets and `import std`. + +```cmake +find_package(libfork CONFIG REQUIRED) + +target_link_libraries(app PRIVATE libfork::libfork) +target_compile_features(app PRIVATE cxx_std_26) +``` + +When building libfork from this source tree, prefer the repository presets above. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..8f9a34598 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,90 @@ +--- +icon: lucide/utensils +--- + +# libfork + +`libfork` is a C++ coroutine-tasking library for strict fork-join parallelism. +It gives programs a small async-function vocabulary, a scheduler-independent +execution model, and worker stacks designed for fine-grained parallel tasks. + +At the top level, users import the library as a C++ module: + +```cpp +import libfork; +``` + +The core idea is simple: a task may fork child tasks, continue with local work, +and then join before reading child results or returning to its own parent. The +runtime uses continuation stealing: the worker that performs a fork continues +with the child, while another worker may steal the parent continuation. + +```cpp +import std; +import libfork; + +struct fib { + template + static auto operator()(lf::env, std::int64_t n) + -> lf::task { + if (n < 2) { + co_return n; + } + + std::int64_t lhs = 0; + std::int64_t rhs = 0; + + auto sc = co_await lf::scope(); + co_await sc.fork(&rhs, fib{}, n - 2); + co_await sc.call(&lhs, fib{}, n - 1); + co_await sc.join(); + + co_return lhs + rhs; + } +}; + +auto main() -> int { + lf::mono_busy_pool> pool{4}; + auto result = lf::schedule(pool, fib{}, 20).get(); + return result == 6765 ? 0 : 1; +} +``` + +## Start here + +- [Getting started](getting-started.md) covers prerequisites, configuration, + building, and a first program. +- [Tour](tour.md) explains the fork-join model, scheduling, cancellation, + exceptions, algorithms, and the stack model. +- [API reference](api/index.md) documents the exported `libfork` modules. +- [Benchmarks](benchmarks.md) describes the benchmark suite. +- [Contributing](contributing.md) lists the local development workflow. + +## Design in one page + +`libfork` tasks are C++ coroutines returning `lf::task`. The first +argument is normally `lf::env`, which lets libfork pass context through +the task graph without constructing user-visible runtime objects. + +Tasks run inside a strict fork-join tree. A fork starts a child that may run in +parallel with the parent continuation. A call starts a child inline and is useful +when there is no profitable continuation left to steal. A join waits for all +outstanding children in the current scope. + +Schedulers are separate from task code. The same task can run on the synchronous +`inline_scheduler`, a monomorphic busy-waiting pool, or a polymorphic pool. The +default practical choice for parallel work today is: + +```cpp +using pool_type = lf::mono_busy_pool>; +``` + +The module surface is intentionally split: + +- `libfork.core` defines tasks, scopes, scheduling, receivers, cancellation, + contexts, handles, projections, and concepts. +- `libfork.batteries` provides worker stacks, deques, context policies, and + context implementations. +- `libfork.schedulers` provides scheduler implementations. +- `libfork.algorithm` provides higher-level fork-join algorithms such as + `for_each` and `fold`. diff --git a/docs/structure.md b/docs/structure.md deleted file mode 100644 index cb1e5203d..000000000 --- a/docs/structure.md +++ /dev/null @@ -1,24 +0,0 @@ -# Structure of libfork - -Libfork is organized into several modules: - -- `libfork`: Meta module that re-exports all public modules - - tuple - - etc -- `libfork.utils`: Independent internal utilities, not part of the public API -- `libfork.core`: Core functionality of libfork including: - - Task template - - Task handles - - Concepts for context/stack/scheduler - - Fork/call primitives - - Execute primitives (for starting work) - - Schedule primitive (for launching work) - - Polymorphic context ABC - - \[internal\] Promise/frame - - \[internal\] Thread locals -- `libfork.batteries`: Collection of context, stack and other types - - The `::stacks` namespace - - Contexts - - adaptors -- `libfork.schedulers`: Collection of schedulers - - Inline scheduler diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 000000000..5d18bf78f --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,9 @@ +.md-typeset .admonition, +.md-typeset details { + font-size: inherit; +} + +.md-typeset .admonition-title, +.md-typeset summary { + font-size: inherit; +} diff --git a/docs/tour.md b/docs/tour.md index 45b7b1fe5..7c08578ba 100644 --- a/docs/tour.md +++ b/docs/tour.md @@ -1,306 +1,176 @@ -# A tour of libfork +--- +icon: lucide/map +--- -TODO: update this page +# Tour -## A tour of libfork +This tour explains how the current module-based libfork API fits together. -This section provides some background and highlights of the `core` API, for details on implementing your own schedulers on-top of libfork see the [extension documentation](https://conorwilliams.github.io/libfork/). Don't forget you can play around with libfork on [godbolt](https://godbolt.org/z/nTeGT34Gv). +## Fork-join tasks -### Contents +Libfork models work as a strict fork-join tree. A task may create children, but +it must join those children before returning. This restriction keeps the task +graph structured and lets the runtime move continuations between workers without +requiring users to manage task lifetimes manually. -- [Fork-join](#fork-join) -- [The cactus stack](#the-cactus-stack) -- [Restrictions on references](#restrictions-on-references) -- [Delaying construction with `lf::eventually`](#delaying-construction) -- [Exception in libfork](#exceptions) -- [Immediate invocation](#immediate-invocation) -- [Explicit scheduling](#explicit-scheduling) -- [Contexts and schedulers](#contexts-and-schedulers) - -### Fork-join - -Definitions: - -- __Task:__ A unit of work that can be executed concurrently with other tasks. -- __Parent:__ A task that spawns other tasks. -- __Child:__ A task that is spawned by another task. - -The tasking/fork-join interface is designed to mirror [Cilk](https://en.wikipedia.org/wiki/Cilk) and other fork-join frameworks. The best way to learn is by example, let's start with the canonical introduction to fork-join, the recursive Fibonacci function, in regular C++ it looks like this: +An async function is any callable that returns `lf::task` when +invoked in a worker context: ```cpp -auto fib(int n) -> int { - - if (n < 2) { - return n; +struct work { + template + static auto operator()(lf::env, int input) -> lf::task { + co_return input * 2; } - - int a = fib(n - 1); - int b = fib(n - 2); - - return a + b; -} +}; ``` -We've already seen how to implement this with libfork in the TLDR but, here it is again with line numbers: - -```cpp - 1| #include "libfork/core.hpp" - 2| - 3| inline constexpr fib = [](auto fib, int n) -> lf::task { - 4| - 5| if (n < 2) { - 6| co_return n; - 7| } - 8| - 9| int a, b; -10| -11| co_await lf::fork[&a, fib](n - 1); -12| co_await lf::call[&b, fib](n - 2); -13| -14| co_await lf::join; -15| -16| co_return a + b; -17| }; -``` +The `lf::env` argument is supplied by libfork. It identifies the worker +context type and allows the same callable to be used with different schedulers. -__NOTE:__ If your compiler does not support the `lf::fork[&a, fib]` syntax then you can use `lf::fork(&a, fib)` and similarly for `lf::call`. +## Fork, call, join -This looks almost like the regular recursive Fibonacci function. However, there are some important differences which we'll explain in a moment. First, the above fibonacci function can be launched on a scheduler, like ``lazy_pool``, as follows: +Inside a task, `co_await lf::scope()` returns a scope object with `fork`, `call`, +`fork_drop`, `call_drop`, and `join`. ```cpp -#include "libfork/schedule.hpp" - -int main() { - - lf::lazy_pool pool(4); // 4 worker threads - - int fib_10 = lf::sync_wait(pool, fib, 10); -} +auto sc = co_await lf::scope(); +co_await sc.fork(&left, child{}, 1); +co_await sc.call(&right, child{}, 2); +co_await sc.join(); ``` -The call to `sync_wait` will block the _main_ thread (i.e. the thread that calls `main()`) until the pool has completed execution of the task. Let's break down what happens after that line by line: - -- __Line 3:__ First we define an _async function_. An async function is a function-object with a templated first argument that returns an `lf::task`. The first argument is used by the library to pass static and dynamic context from parent to child. Additionally, it acts as a [y-combinator](https://en.wikipedia.org/wiki/Fixed-point_combinator) - allowing the lambda to be recursive - and provides a few methods which we will discuss later. -- __Line 9:__ Next we construct the variables that will be bound to the return values of following forks/calls. -- __Line 11:__ This is the first call to `lf::fork` which marks the beginning of an _async scope_. `lf::fork[&a, fib]` binds the return address of the function `fib` to the integer `a`. Internally the child coroutine will have to store a pointer to the return variable so we make this explicit at the call site. The bound function is then invoked with the argument `n - 1`. The semantics of all of this is: the execution of the forked function (in this case `fib`) can continue concurrently with the execution of the next line of code i.e. the _continuation_. As libfork is a continuation stealing library the worker/thread that performed the fork will immediately begin executing the forked function while another thread may _steal_ the continuation. -- __Line 12:__ An `lf::call` binds arguments and return address in the same way as `lf::fork` however, it has the semantics of a serial function call. This is done instead of an `lf::fork` as there is no further work to do in the current task so stealing it would be a waste of resources. -- __Line 13:__ Execution cannot continue past a join-point until all child tasks have completed. After this point it is safe to access the results (`a` and `b`) of the child task. Only a single worker will continue execution after the join. This marks the end of the async scope that began at the `fork`. -- __Line 16:__ Finally we return the result of the to a parent task, this has similar semantics to a regular return however, behind the scenes an assignment of the return value to the parent's return address is performed. This is the end of the async function. The worker will attempt to resume the parent task (if it has not already been stolen) just as a regular function would resume execution of the caller. - -__NOTE:__ At every ``co_await`` the OS-thread executing the task may change! - -__NOTE:__ Libfork implements _strict_ fork-join which means all children __must__ be joined __before__ a task returns. This restriction give some nice mathematical properties to the underlying directed acyclic graph (DAG) of tasks that enables many optimizations. - -#### Ignoring a result +`fork` exposes the parent continuation for stealing and immediately starts the +child on the current worker. `call` starts the child inline and is useful when +there is no useful continuation left to steal. `join` waits until all children +created through the scope have finished. -If you wanted to ignore the result of a fork/call (i.e. if you wanted the side effect only) you can simply omit return address from lines 11 and 12 e.g.: +Use the `_drop` variants when the child result is intentionally ignored: ```cpp -co_await lf::fork[fib](n - 1); -co_await lf::call[fib](n - 2); +co_await sc.fork_drop(side_effect{}, item); +co_await sc.call_drop(cleanup{}, item); ``` -### The cactus-stack +## Result storage -Normally each call to a coroutine would allocate on the heap. However, libfork implements a cactus-stack - supported by segmented-stacks - which allows each coroutine to be allocated on a fragment of linear stack, this has almost the same overhead as allocating on the real stack. This means the overhead of a fork/call in libfork is very low compared to most traditional library-based implementations (about 10x the overhead of a bare function call). - -The internal cactus-stack is exposed to the user via the `co_new` function: +Non-void child results are written into caller-provided storage: ```cpp -inline constexpr auto co_new_demo = [](auto co_new_demo, std::span inputs) -> lf::task { - - // Allocate space for results, outputs is a std::span - auto [outputs] = co_await lf::co_new(inputs.size()); - - // Launch a task for each input. - for(std::size_t i = 0; i < inputs.size(); ++i) { - co_await lf::fork[&outputs[i], some_function](inputs[i]); - } - - co_await lf::join; // Wait for all tasks to complete. - - co_return std::accumulate(outputs.begin(), outputs.end(), 0); -}; +int result = 0; +co_await sc.fork(&result, compute{}, input); +co_await sc.join(); ``` -Here the `co_await` on the result of `lf::co_new` returns an immovable RAII class which will manage the lifetime of the allocation. +The pointer must remain valid until after the join. A common pattern is to keep +child result variables in the parent coroutine frame and read them only after +`join`. -### Restrictions on references +## Continuation stealing -References as inputs to coroutines can be error prone, for example: +On a fork, libfork pushes a handle to the parent continuation into the worker +context and runs the child immediately. Another worker may steal that +continuation and resume it. This differs from child-stealing runtimes, where the +new child is normally offered to other workers. -```cpp -co_await lf::fork[process_string](std::string("32")); -``` +The important user-facing consequence is that execution may resume on a +different OS thread after any `co_await`. Code inside tasks should not assume +thread affinity unless it uses an explicit scheduling awaitable. -This would dangle if `process_string` accepted arguments by reference. Specifically a `process_string` accepting `std::string &` would not compile by the standard reference semantics while `std::string const &` and `std::string &&` would compile but would dangle. To avoid this libfork coroutines bans `std::string && -> std::string const &` conversions and r-value reference arguments for forked async-functions. If you want to move a value into a forked coroutine then pass by value. +## Worker stacks -__Note:__ You can still dangle by ending the lifetime of an l-value referenced object __after__ a fork e.g.: +Coroutine frames created by fork/call are allocated from a worker stack. The +provided stacks trade simplicity, speed, and bounded memory: -```cpp -{ - int x; +- `geometric_stack` is the general-purpose segmented stack. +- `slab_stack` uses a fixed-capacity slab and throws `std::bad_alloc` on + overflow. +- `adaptor_stack` delegates every push/pop to an allocator. - co_await lf::fork[&x, some_function](); +Schedulers combine a stack with a context policy, such as `adapt_vector` for a +single-threaded inline scheduler or `adapt_deque` for stealing between workers. -} // Lifetime of x ends here, return address is now dangling! +## Exceptions -co_await lf::join; -``` - -### Delaying construction - -Some types are expensive or impossible to default construct, for these instances libfork provides the `lf::eventually` template type. `lf::eventually` functions like a `std::optional` that is only constructed once and supports references: +If a child task exits with an exception, libfork stores the exception in the +parent and rethrows it at `join`. ```cpp -// Not default constructible. -struct difficult { - difficult(int) {} -}; - -// Async function that returns a difficult. -inline constexpr auto make_difficult = [](auto) -> lf::task { - co_return 42; -} - -// Async function that returns a reference. -inline constexpr auto reference = [](auto) -> lf::task { - co_return /* some reference */; -} - -inline constexpr auto eventually_demo = [](auto) -> lf::task<> { - - // Use lf::eventually to delay construction. - lf::eventually a; - lf::eventually b; - - co_await lf::fork[&a, make_difficult](); - co_await lf::fork[&b, reference](); - - co_await lf::join; - - std::cout << *b << std::endl; // lf::eventually support operators * and -> -}; +auto sc = co_await lf::scope(); +co_await sc.fork_drop(may_throw{}, input); +co_await sc.join(); // rethrows if the child failed ``` -### Exceptions +Because libfork is strict fork-join, task code should structure potentially +throwing work so outstanding children are still joined before the task exits. +When in doubt, join in the same lexical region that created the children. -Libfork supports exceptions in async functions. If an exception escapes an async function then it will be stored in its parent and re-thrown when the parent reaches a join-point. For example: +`receiver::get()` also rethrows exceptions from a scheduled root task. -```cpp -inline constexpr auto exception_demo = [](auto) -> lf::task<> { - - co_await lf::fork[throwing_work](/* args.. */); - co_await lf::fork[throwing_work](/* args.. */); - - co_await lf::join; // Will (re)throw one of the exceptions from the children. -}; -``` +## Cancellation -However, you need to be very careful when throwing exception inside a fork-join scope because it's UB for a task which has forked children to return (regularly or by exception) without first calling `lf::join`. For example: +Use `lf::child_scope()` to create a scope with its own `stop_source`. Children +launched through that scope inherit its stop token. ```cpp -inline constexpr auto bad_code = [](auto) -> lf::task<> { - - co_await lf::fork[work](/* args.. */); - - function_which_could_throw(); // UB on exception! No join before return. - - co_await lf::join; +struct maybe_run { + template + static auto operator()(lf::env) -> lf::task { + auto sc = co_await lf::child_scope(); + sc.request_stop(); + co_await sc.fork_drop(child_work{}); + co_await sc.join(); + } }; ``` -Instead you must wrap your potentially throwing code in a try-catch block and call `lf::join`. +For root tasks, construct `lf::recv_state` and pass it to `schedule`. +The returned `lf::receiver` exposes `stop_source()`: ```cpp -inline constexpr auto good_code = [](auto good_code) -> lf::task<> { - - co_await lf::fork[&a, work](/* args.. */); - - try { - function_which_could_throw(); - } catch (...) { - good_code.stash_exception(); // Store's exception. - } - - co_await lf::join; // Exception from child or stashed-exception will be re-thrown here. -}; +lf::recv_state state; +auto recv = lf::schedule(pool, std::move(state), root_task{}); +recv.stop_source().request_stop(); ``` -If/when C++ adds asynchronous RAII then this will be made much cleaner. +When a cancellable receiver is consumed after cancellation, +`receiver::get()` throws `lf::operation_cancelled_error`. -If you would like to capture exceptions for each child individually then you can use a return object that supports capturing exceptions, for example: +## Explicit scheduling -```cpp -inline constexpr auto exception_stash_demo = [](auto) -> lf::task<> { - - try_eventually ret; - - co_await lf::fork[&ret, int_or_throw](/* args.. */); - - co_await lf::join; // Will not throw, exception stored in ret. - - if (ret.has_exception()) { - // Handle exception. - } else { - // Handle result. - } -}; -``` - -Any return pointer which satisfies the `stash_exception_in_return` concept will trigger libfork to store the exception in the return object. This concept is specified as follows: +Libfork supports context-switching awaitables. A type is an `lf::awaitable` when it can be acquired through `operator co_await` and its awaitable +has: ```cpp -template -concept stash_exception_in_return = lf::quasi_pointer && requires (I ptr) { - { stash_exception(*ptr) } noexcept; -}; +auto await_ready() -> bool; +auto await_suspend(lf::sched_handle, Context&) -> void; +auto await_resume(); ``` -__Note:__ the call to `stash_exception` must be `noexcept`. - -### Immediate invocation +`await_suspend` receives a schedulable handle and the current context. It may +post that handle to another scheduler, allowing a task to hop between pools. -Sometimes you may want to just call an async function without a fork join scope, for example: +## Algorithms -```cpp -int result; - -co_await lf::call[&result, some_function](/* args.. */); - -co_await lf::join; // Still needed in-case of exceptions -``` +The algorithm module provides fork-join operations over random-access ranges. -In this case you could simplify the above with `lf::just`: +`lf::for_each` applies a synchronous or asynchronous function to every element: ```cpp -int result = co_await lf::just[some_function](/* args.. */); +auto recv = lf::schedule(pool, lf::for_each, std::span(values), [](int& x) { + x *= 2; +}); +std::move(recv).get(); ``` -### Explicit scheduling +`lf::fold` reduces a non-empty range to `std::optional`, returning +`std::nullopt` for empty input: -Normally in libfork _where_ a task is being executed is controlled by the runtime. However, you may want to explicitly schedule a task to be resumed on a certain worker or write an awaitable that transfers execution to a different pool of workers. This is made possible through the `context_switcher` API. Instead of writing a regular awaitable, write one that conforms to the `context_switcher` concept, like this: - ```cpp -struct my_special_awaitable { - auto await_ready() -> bool; - auto await_suspend(lf::submit_handle handle) -> void; - auto await_resume() -> /* [T] */; -}; +auto recv = lf::schedule(pool, lf::fold, std::span(values), std::plus<>{}); +auto sum = std::move(recv).get(); ``` -This can be `co_await`ed inside a libfork task, if `await_ready` returns `false` then the task will be suspended and `await_suspend` will be called with a handle to the suspended task, this can be resumed by any worker you like. - -This is used by libfork's `template auto resume_on(T *)` to enable explicit scheduling. - -### Contexts and schedulers - -We have already encountered a scheduler in the [fork-join](#fork-join) however, we have not yet discussed what a scheduler is or how to implement one. A scheduler is a type that conforms to the `lf::scheduler` concept, this is a customization point that allows you to implement your own scheduling strategy. This makes a type suitable for use with `lf::sync_wait`. Have a look at the [extensions api](https://conorwilliams.github.io/libfork/) for further details. - -Three schedulers are provided by libfork: - -- [`lf::lazy_pool`](https://conorwilliams.github.io/libfork/api/schedule.html#lazy-pool) A NUMA-aware work-stealing scheduler that is suitable for general use. This should be the default choice for most applications. -- [`lf::busy_pool`](https://conorwilliams.github.io/libfork/api/schedule.html#busy-pool) Also a NUMA-aware work-stealing scheduler however, workers will busy-wait for work instead of sleeping. This often gains very little performance over `lf::lazy_pool` and should only be preferred if you have an otherwise idle machine and you are willing to sacrifice a lot of power consumption for very little performance. -- [`lf::unit_pool`](https://conorwilliams.github.io/libfork/api/schedule.html#lazy-pool) A is single threaded scheduler that is suitable for testing and debugging. - -__NOTE:__ The workers inside libfork's thread pools should never block i.e. __do not__ call `sync_wait` or any other blocking function inside a `task`. +Both algorithms accept iterator/sentinel pairs or ranges. Overloads with an +explicit chunk size require that size to be positive. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..9bc1e7df1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "libfork" +version = "0.1.0" +description = "Documentation for libfork" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "zensical>=0.0.41", +] diff --git a/src/core/task.cxx b/src/core/task.cxx index 424910b4e..17a45eebd 100644 --- a/src/core/task.cxx +++ b/src/core/task.cxx @@ -27,19 +27,6 @@ struct promise_type; /** * @brief The return type for libfork's async functions/coroutines. - * - * This predominantly exists to disambiguate `libfork`s coroutines from other - * coroutines and specify `T` the async function's return type which is - * required to be `void` or a `std::movable` type. - * - * \rst - * - * .. note:: - * - * No consumer of this library should ever touch an instance of this type, - * it is used for specifying the return type of an `async` function only. - * - * \endrst */ export template class task { diff --git a/tour.md b/tour.md new file mode 100644 index 000000000..45b7b1fe5 --- /dev/null +++ b/tour.md @@ -0,0 +1,306 @@ +# A tour of libfork + +TODO: update this page + +## A tour of libfork + +This section provides some background and highlights of the `core` API, for details on implementing your own schedulers on-top of libfork see the [extension documentation](https://conorwilliams.github.io/libfork/). Don't forget you can play around with libfork on [godbolt](https://godbolt.org/z/nTeGT34Gv). + +### Contents + +- [Fork-join](#fork-join) +- [The cactus stack](#the-cactus-stack) +- [Restrictions on references](#restrictions-on-references) +- [Delaying construction with `lf::eventually`](#delaying-construction) +- [Exception in libfork](#exceptions) +- [Immediate invocation](#immediate-invocation) +- [Explicit scheduling](#explicit-scheduling) +- [Contexts and schedulers](#contexts-and-schedulers) + +### Fork-join + +Definitions: + +- __Task:__ A unit of work that can be executed concurrently with other tasks. +- __Parent:__ A task that spawns other tasks. +- __Child:__ A task that is spawned by another task. + +The tasking/fork-join interface is designed to mirror [Cilk](https://en.wikipedia.org/wiki/Cilk) and other fork-join frameworks. The best way to learn is by example, let's start with the canonical introduction to fork-join, the recursive Fibonacci function, in regular C++ it looks like this: + +```cpp +auto fib(int n) -> int { + + if (n < 2) { + return n; + } + + int a = fib(n - 1); + int b = fib(n - 2); + + return a + b; +} +``` + +We've already seen how to implement this with libfork in the TLDR but, here it is again with line numbers: + +```cpp + 1| #include "libfork/core.hpp" + 2| + 3| inline constexpr fib = [](auto fib, int n) -> lf::task { + 4| + 5| if (n < 2) { + 6| co_return n; + 7| } + 8| + 9| int a, b; +10| +11| co_await lf::fork[&a, fib](n - 1); +12| co_await lf::call[&b, fib](n - 2); +13| +14| co_await lf::join; +15| +16| co_return a + b; +17| }; +``` + +__NOTE:__ If your compiler does not support the `lf::fork[&a, fib]` syntax then you can use `lf::fork(&a, fib)` and similarly for `lf::call`. + +This looks almost like the regular recursive Fibonacci function. However, there are some important differences which we'll explain in a moment. First, the above fibonacci function can be launched on a scheduler, like ``lazy_pool``, as follows: + +```cpp +#include "libfork/schedule.hpp" + +int main() { + + lf::lazy_pool pool(4); // 4 worker threads + + int fib_10 = lf::sync_wait(pool, fib, 10); +} +``` + +The call to `sync_wait` will block the _main_ thread (i.e. the thread that calls `main()`) until the pool has completed execution of the task. Let's break down what happens after that line by line: + +- __Line 3:__ First we define an _async function_. An async function is a function-object with a templated first argument that returns an `lf::task`. The first argument is used by the library to pass static and dynamic context from parent to child. Additionally, it acts as a [y-combinator](https://en.wikipedia.org/wiki/Fixed-point_combinator) - allowing the lambda to be recursive - and provides a few methods which we will discuss later. +- __Line 9:__ Next we construct the variables that will be bound to the return values of following forks/calls. +- __Line 11:__ This is the first call to `lf::fork` which marks the beginning of an _async scope_. `lf::fork[&a, fib]` binds the return address of the function `fib` to the integer `a`. Internally the child coroutine will have to store a pointer to the return variable so we make this explicit at the call site. The bound function is then invoked with the argument `n - 1`. The semantics of all of this is: the execution of the forked function (in this case `fib`) can continue concurrently with the execution of the next line of code i.e. the _continuation_. As libfork is a continuation stealing library the worker/thread that performed the fork will immediately begin executing the forked function while another thread may _steal_ the continuation. +- __Line 12:__ An `lf::call` binds arguments and return address in the same way as `lf::fork` however, it has the semantics of a serial function call. This is done instead of an `lf::fork` as there is no further work to do in the current task so stealing it would be a waste of resources. +- __Line 13:__ Execution cannot continue past a join-point until all child tasks have completed. After this point it is safe to access the results (`a` and `b`) of the child task. Only a single worker will continue execution after the join. This marks the end of the async scope that began at the `fork`. +- __Line 16:__ Finally we return the result of the to a parent task, this has similar semantics to a regular return however, behind the scenes an assignment of the return value to the parent's return address is performed. This is the end of the async function. The worker will attempt to resume the parent task (if it has not already been stolen) just as a regular function would resume execution of the caller. + +__NOTE:__ At every ``co_await`` the OS-thread executing the task may change! + +__NOTE:__ Libfork implements _strict_ fork-join which means all children __must__ be joined __before__ a task returns. This restriction give some nice mathematical properties to the underlying directed acyclic graph (DAG) of tasks that enables many optimizations. + +#### Ignoring a result + +If you wanted to ignore the result of a fork/call (i.e. if you wanted the side effect only) you can simply omit return address from lines 11 and 12 e.g.: + +```cpp +co_await lf::fork[fib](n - 1); +co_await lf::call[fib](n - 2); +``` + +### The cactus-stack + +Normally each call to a coroutine would allocate on the heap. However, libfork implements a cactus-stack - supported by segmented-stacks - which allows each coroutine to be allocated on a fragment of linear stack, this has almost the same overhead as allocating on the real stack. This means the overhead of a fork/call in libfork is very low compared to most traditional library-based implementations (about 10x the overhead of a bare function call). + +The internal cactus-stack is exposed to the user via the `co_new` function: + +```cpp +inline constexpr auto co_new_demo = [](auto co_new_demo, std::span inputs) -> lf::task { + + // Allocate space for results, outputs is a std::span + auto [outputs] = co_await lf::co_new(inputs.size()); + + // Launch a task for each input. + for(std::size_t i = 0; i < inputs.size(); ++i) { + co_await lf::fork[&outputs[i], some_function](inputs[i]); + } + + co_await lf::join; // Wait for all tasks to complete. + + co_return std::accumulate(outputs.begin(), outputs.end(), 0); +}; +``` + +Here the `co_await` on the result of `lf::co_new` returns an immovable RAII class which will manage the lifetime of the allocation. + +### Restrictions on references + +References as inputs to coroutines can be error prone, for example: + +```cpp +co_await lf::fork[process_string](std::string("32")); +``` + +This would dangle if `process_string` accepted arguments by reference. Specifically a `process_string` accepting `std::string &` would not compile by the standard reference semantics while `std::string const &` and `std::string &&` would compile but would dangle. To avoid this libfork coroutines bans `std::string && -> std::string const &` conversions and r-value reference arguments for forked async-functions. If you want to move a value into a forked coroutine then pass by value. + +__Note:__ You can still dangle by ending the lifetime of an l-value referenced object __after__ a fork e.g.: + +```cpp +{ + int x; + + co_await lf::fork[&x, some_function](); + +} // Lifetime of x ends here, return address is now dangling! + +co_await lf::join; +``` + +### Delaying construction + +Some types are expensive or impossible to default construct, for these instances libfork provides the `lf::eventually` template type. `lf::eventually` functions like a `std::optional` that is only constructed once and supports references: + +```cpp +// Not default constructible. +struct difficult { + difficult(int) {} +}; + +// Async function that returns a difficult. +inline constexpr auto make_difficult = [](auto) -> lf::task { + co_return 42; +} + +// Async function that returns a reference. +inline constexpr auto reference = [](auto) -> lf::task { + co_return /* some reference */; +} + +inline constexpr auto eventually_demo = [](auto) -> lf::task<> { + + // Use lf::eventually to delay construction. + lf::eventually a; + lf::eventually b; + + co_await lf::fork[&a, make_difficult](); + co_await lf::fork[&b, reference](); + + co_await lf::join; + + std::cout << *b << std::endl; // lf::eventually support operators * and -> +}; +``` + +### Exceptions + +Libfork supports exceptions in async functions. If an exception escapes an async function then it will be stored in its parent and re-thrown when the parent reaches a join-point. For example: + +```cpp +inline constexpr auto exception_demo = [](auto) -> lf::task<> { + + co_await lf::fork[throwing_work](/* args.. */); + co_await lf::fork[throwing_work](/* args.. */); + + co_await lf::join; // Will (re)throw one of the exceptions from the children. +}; +``` + +However, you need to be very careful when throwing exception inside a fork-join scope because it's UB for a task which has forked children to return (regularly or by exception) without first calling `lf::join`. For example: + +```cpp +inline constexpr auto bad_code = [](auto) -> lf::task<> { + + co_await lf::fork[work](/* args.. */); + + function_which_could_throw(); // UB on exception! No join before return. + + co_await lf::join; +}; +``` + +Instead you must wrap your potentially throwing code in a try-catch block and call `lf::join`. + +```cpp +inline constexpr auto good_code = [](auto good_code) -> lf::task<> { + + co_await lf::fork[&a, work](/* args.. */); + + try { + function_which_could_throw(); + } catch (...) { + good_code.stash_exception(); // Store's exception. + } + + co_await lf::join; // Exception from child or stashed-exception will be re-thrown here. +}; +``` + +If/when C++ adds asynchronous RAII then this will be made much cleaner. + +If you would like to capture exceptions for each child individually then you can use a return object that supports capturing exceptions, for example: + +```cpp +inline constexpr auto exception_stash_demo = [](auto) -> lf::task<> { + + try_eventually ret; + + co_await lf::fork[&ret, int_or_throw](/* args.. */); + + co_await lf::join; // Will not throw, exception stored in ret. + + if (ret.has_exception()) { + // Handle exception. + } else { + // Handle result. + } +}; +``` + +Any return pointer which satisfies the `stash_exception_in_return` concept will trigger libfork to store the exception in the return object. This concept is specified as follows: + +```cpp +template +concept stash_exception_in_return = lf::quasi_pointer && requires (I ptr) { + { stash_exception(*ptr) } noexcept; +}; +``` + +__Note:__ the call to `stash_exception` must be `noexcept`. + +### Immediate invocation + +Sometimes you may want to just call an async function without a fork join scope, for example: + +```cpp +int result; + +co_await lf::call[&result, some_function](/* args.. */); + +co_await lf::join; // Still needed in-case of exceptions +``` + +In this case you could simplify the above with `lf::just`: + +```cpp +int result = co_await lf::just[some_function](/* args.. */); +``` + +### Explicit scheduling + +Normally in libfork _where_ a task is being executed is controlled by the runtime. However, you may want to explicitly schedule a task to be resumed on a certain worker or write an awaitable that transfers execution to a different pool of workers. This is made possible through the `context_switcher` API. Instead of writing a regular awaitable, write one that conforms to the `context_switcher` concept, like this: + +```cpp +struct my_special_awaitable { + auto await_ready() -> bool; + auto await_suspend(lf::submit_handle handle) -> void; + auto await_resume() -> /* [T] */; +}; +``` + +This can be `co_await`ed inside a libfork task, if `await_ready` returns `false` then the task will be suspended and `await_suspend` will be called with a handle to the suspended task, this can be resumed by any worker you like. + +This is used by libfork's `template auto resume_on(T *)` to enable explicit scheduling. + +### Contexts and schedulers + +We have already encountered a scheduler in the [fork-join](#fork-join) however, we have not yet discussed what a scheduler is or how to implement one. A scheduler is a type that conforms to the `lf::scheduler` concept, this is a customization point that allows you to implement your own scheduling strategy. This makes a type suitable for use with `lf::sync_wait`. Have a look at the [extensions api](https://conorwilliams.github.io/libfork/) for further details. + +Three schedulers are provided by libfork: + +- [`lf::lazy_pool`](https://conorwilliams.github.io/libfork/api/schedule.html#lazy-pool) A NUMA-aware work-stealing scheduler that is suitable for general use. This should be the default choice for most applications. +- [`lf::busy_pool`](https://conorwilliams.github.io/libfork/api/schedule.html#busy-pool) Also a NUMA-aware work-stealing scheduler however, workers will busy-wait for work instead of sleeping. This often gains very little performance over `lf::lazy_pool` and should only be preferred if you have an otherwise idle machine and you are willing to sacrifice a lot of power consumption for very little performance. +- [`lf::unit_pool`](https://conorwilliams.github.io/libfork/api/schedule.html#lazy-pool) A is single threaded scheduler that is suitable for testing and debugging. + +__NOTE:__ The workers inside libfork's thread pools should never block i.e. __do not__ call `sync_wait` or any other blocking function inside a `task`. diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..6c399ac61 --- /dev/null +++ b/uv.lock @@ -0,0 +1,241 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "deepmerge" +version = "2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890, upload-time = "2024-08-30T05:31:50.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "libfork" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "zensical" }, +] + +[package.metadata] +requires-dist = [{ name = "zensical", specifier = ">=0.0.41" }] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "zensical" +version = "0.0.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/d6/b3e931233e53a2377ef5915cc6e786845c3263306874a469af8fb569ef9c/zensical-0.0.41.tar.gz", hash = "sha256:6c3c90301123749dfc26a210d6c080f0691253c7c765ad308a10b4518369a6fe", size = 3927788, upload-time = "2026-05-09T14:35:29.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/08/ee18207c9b4e3ada74a0f4adf253bea90da39ae43772761cd91072e3a1fc/zensical-0.0.41-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f06a0015dcfdf7aeca73f4998a401db65db0ae2dd72da9629a7be8f9a4d0b7b6", size = 12701539, upload-time = "2026-05-09T14:34:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/4c/93/d4635fbbce8171cf71dd64285d9f6d5773a2b624b928f1dd8acaf1ee9f9f/zensical-0.0.41-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:4e524ce68c9ff082ffaded9f742407097cf51bab692b7bc18d3c174b966174fe", size = 12560038, upload-time = "2026-05-09T14:34:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/1730a30377bbb0914ed740e0e289d379b0552673b6cf912aefe7a205440c/zensical-0.0.41-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4afe35331cd2394c408cd362458936479cc0ed4fb272478498e4794aafc7414", size = 12942926, upload-time = "2026-05-09T14:34:54.393Z" }, + { url = "https://files.pythonhosted.org/packages/32/e3/d9a0416ef4edc043ce9f404a66f1934f102bcb645b103abb26b180ba5680/zensical-0.0.41-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15a850285050f03aeb3b67ce7d99943093059fe8d32fc7731fa9f27be45c64cc", size = 12912711, upload-time = "2026-05-09T14:34:57.174Z" }, + { url = "https://files.pythonhosted.org/packages/68/d0/775852783bef835425306a2fcd8236ef14fd19160e1b4261e192bf2d9f54/zensical-0.0.41-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35052e9dbefabe3a71c4836cfc4afa6c9469e5eeddc2a3ee750803ae3fe777dc", size = 13275869, upload-time = "2026-05-09T14:34:59.93Z" }, + { url = "https://files.pythonhosted.org/packages/c3/95/554273cc09a270ced0213d3e0aac8b3fc2b472fc2b26771d56fc8fd55047/zensical-0.0.41-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47f459205fb55f64dcb6c65e9f3c2fa00a2b4306c5ef1b71b9a50c45007071d", size = 12980177, upload-time = "2026-05-09T14:35:02.81Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b5/d74d5040b3121db5c72b0134f0455641b90b1277fb1330a8e5e0029ca8d3/zensical-0.0.41-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:aa3b3b3a4e6f75f6bb3c1aca1fad7a96cebf54cbd4e31122f6876503b8801666", size = 13119629, upload-time = "2026-05-09T14:35:07.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/9a/93527acd7750092d7fca2e6c43fe2b8f1e85e1c96a1002baf6a08201c6f7/zensical-0.0.41-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:565133fd48b2ce939698c174c0c1c6470407a8fb6a90a2bb0eeec97cd4344444", size = 13182183, upload-time = "2026-05-09T14:35:10.105Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/d77e4c809bfcbad40db85a6a7beeda2ee5c964232e0186783c3a837a7d0b/zensical-0.0.41-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:cec0a2b05eaaace0c7424bab3f2884da03ade212cac4ba4487c58691ec13ec65", size = 13330444, upload-time = "2026-05-09T14:35:13.245Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/ecbb7e34bff88aa892c676b8b2e2ddf425f94d66cbb84b80016095191b77/zensical-0.0.41-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1736f0cb7686628cc6f53952d208423f20b542f0c16b0c2ddd7e702bf6e41fdd", size = 13263093, upload-time = "2026-05-09T14:35:20.826Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6f/48b2f81ce708d19bb807d94716f2772ec4b74389b6d29024669fc470df08/zensical-0.0.41-cp310-abi3-win32.whl", hash = "sha256:34a78645c68fba152faacb66516c895283166154f8b15b61440a6c21c84f0974", size = 12253644, upload-time = "2026-05-09T14:35:23.598Z" }, + { url = "https://files.pythonhosted.org/packages/a0/92/5cf943133f61b996965743deeaff467f278135521f58d83ca68d2601ded3/zensical-0.0.41-cp310-abi3-win_amd64.whl", hash = "sha256:00d80cd573152e0efb655143bbdfe8788eb4b33167a802639fdb1b1800b724ac", size = 12483190, upload-time = "2026-05-09T14:35:26.43Z" }, +] diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 000000000..09db30dec --- /dev/null +++ b/zensical.toml @@ -0,0 +1,323 @@ +[project] + +# The site_name is shown in the page header and the browser window title +# +# Read more: https://zensical.org/docs/setup/basics/#site_name +site_name = "libfork" + +# The site_description is included in the HTML head and should contain a +# meaningful description of the site content for use by search engines. +# +# Read more: https://zensical.org/docs/setup/basics/#site_description +site_description = "A bleeding-edge, lock-free, wait-free, continuation-stealing tasking library built on C++20's coroutines" + +# The site_author attribute. This is used in the HTML head element. +# +# Read more: https://zensical.org/docs/setup/basics/#site_author +site_author = "Conor Williams" + +# The site_url is the canonical URL for your site. When building online +# documentation you should set this. +# Read more: https://zensical.org/docs/setup/basics/#site_url +site_url = "https://conorwilliams.github.io/libfork/" + +# The copyright notice appears in the page footer and can contain an HTML +# fragment. +# +# Read more: https://zensical.org/docs/setup/basics/#copyright +copyright = """ +Copyright © 2026 Conor Williams. +""" + +# Directory of the site artifacts generated by the build command. +site_dir = "build/site" + +# Additional stylesheets, relative to the docs directory. +extra_css = ["stylesheets/extra.css"] + +# Inform that this is a docs site. +repo_url = "https://github.com/conorwilliams/libfork" + +# Set branch for edit/view links +# TODO: make this point to main +edit_uri = "edit/modules/docs/" + +# Explicit sidebar order. Paths are relative to the docs directory. +nav = [ + { "Home" = "index.md" }, + { "Getting Started" = "getting-started.md" }, + { "Tour" = "tour.md" }, + { "API" = [ + "api/index.md", + { "Core" = [ + "api/core/index.md", + "api/core/task.md", + "api/core/env.md", + "api/core/scope.md", + "api/core/scheduling.md", + "api/core/receiver.md", + "api/core/cancellation.md", + "api/core/context.md", + "api/core/handles.md", + "api/core/projected.md", + "api/core/concepts.md", + "api/core/exceptions.md", + ] }, + { "Batteries" = "api/batteries.md" }, + { "Schedulers" = "api/schedulers.md" }, + { "Algorithm" = "api/algorithm.md" }, + ] }, + { "Benchmarks" = "benchmarks.md" }, + { "Contributing" = "contributing.md" }, +] + +# ---------------------------------------------------------------------------- +# Section for configuring theme options +# ---------------------------------------------------------------------------- +[project.theme] + +# With the "favicon" option you can set your own image to use as the icon +# browsers will use in the browser title bar or tab bar. The path provided +# must be relative to the "docs_dir". +# +# Read more: +# - https://zensical.org/docs/setup/logo-and-icons/#favicon +# - https://developer.mozilla.org/en-US/docs/Glossary/Favicon +# +favicon = "favicon/favicon.ico" + +# Zensical supports more than 60 different languages. This means that the +# labels and tooltips that Zensical's templates produce are translated. +# The "language" option allows you to set the language used. This language +# is also indicated in the HTML head element to help with accessibility +# and guide search engines and translation tools. +# +# The default language is "en" (English). It is possible to create +# sites with multiple languages and configure a language selector. See +# the documentation for details. +# +# Read more: +# - https://zensical.org/docs/setup/language/ +# +language = "en" + +# Zensical provides a number of feature toggles that change the behavior +# of the documentation site. +features = [ + # Zensical includes an announcement bar. This feature allows users to + # dismiss it when they have read the announcement. + # https://zensical.org/docs/setup/header/#announcement-bar + "announce.dismiss", + + # If you have a repository configured and turn on this feature, Zensical + # will generate an edit button for the page. This works for common + # repository hosting services. + # https://zensical.org/docs/setup/repository/#content-actions + "content.action.edit", + + # If you have a repository configured and turn on this feature, Zensical + # will generate a button that allows the user to view the Markdown + # code for the current page. + # https://zensical.org/docs/setup/repository/#content-actions + "content.action.view", + + # Code annotations allow you to add an icon with a tooltip to your + # code blocks to provide explanations at crucial points. + # https://zensical.org/docs/authoring/code-blocks/#code-annotations + "content.code.annotate", + + # This feature turns on a button in code blocks that allow users to + # copy the content to their clipboard without first selecting it. + # https://zensical.org/docs/authoring/code-blocks/#code-copy-button + "content.code.copy", + + # Code blocks can include a button to allow for the selection of line + # ranges by the user. + # https://zensical.org/docs/authoring/code-blocks/#code-selection-button + "content.code.select", + + # Zensical can render footnotes as inline tooltips, so the user can read + # the footnote without leaving the context of the document. + # https://zensical.org/docs/authoring/footnotes/#footnote-tooltips + "content.footnote.tooltips", + + # If you have many content tabs that have the same titles (e.g., "Python", + # "JavaScript", "Cobol"), this feature causes all of them to switch to + # at the same time when the user chooses their language in one. + # https://zensical.org/docs/authoring/content-tabs/#linked-content-tabs + "content.tabs.link", + + # With this feature enabled users can add tooltips to links that will be + # displayed when the mouse pointer hovers the link. + # https://zensical.org/docs/authoring/tooltips/#improved-tooltips + "content.tooltips", + + # With this feature enabled, Zensical will automatically hide parts + # of the header when the user scrolls past a certain point. + # https://zensical.org/docs/setup/header/#automatic-hiding + # "header.autohide", + + # Turn on this feature to expand all collapsible sections in the + # navigation sidebar by default. + # https://zensical.org/docs/setup/navigation/#navigation-expansion + # "navigation.expand", + + # This feature turns on navigation elements in the footer that allow the + # user to navigate to a next or previous page. + # https://zensical.org/docs/setup/footer/#navigation + "navigation.footer", + + # When section index pages are enabled, documents can be directly attached + # to sections, which is particularly useful for providing overview pages. + # https://zensical.org/docs/setup/navigation/#section-index-pages + "navigation.indexes", + + # When instant navigation is enabled, clicks on all internal links will be + # intercepted and dispatched via XHR without fully reloading the page. + # https://zensical.org/docs/setup/navigation/#instant-navigation + "navigation.instant", + + # With instant prefetching, your site will start to fetch a page once the + # user hovers over a link. This will reduce the perceived loading time + # for the user. + # https://zensical.org/docs/setup/navigation/#instant-prefetching + "navigation.instant.prefetch", + + # In order to provide a better user experience on slow connections when + # using instant navigation, a progress indicator can be enabled. + # https://zensical.org/docs/setup/navigation/#progress-indicator + #"navigation.instant.progress", + + # When navigation paths are activated, a breadcrumb navigation is rendered + # above the title of each page + # https://zensical.org/docs/setup/navigation/#navigation-path + "navigation.path", + + # When pruning is enabled, only the visible navigation items are included + # in the rendered HTML, reducing the size of the built site by 33% or more. + # https://zensical.org/docs/setup/navigation/#navigation-pruning + #"navigation.prune", + + # When sections are enabled, top-level sections are rendered as groups in + # the sidebar for viewports above 1220px, but remain as-is on mobile. + # https://zensical.org/docs/setup/navigation/#navigation-sections + "navigation.sections", + + # When tabs are enabled, top-level sections are rendered in a menu layer + # below the header for viewports above 1220px, but remain as-is on mobile. + # https://zensical.org/docs/setup/navigation/#navigation-tabs + #"navigation.tabs", + + # When sticky tabs are enabled, navigation tabs will lock below the header + # and always remain visible when scrolling down. + # https://zensical.org/docs/setup/navigation/#sticky-navigation-tabs + #"navigation.tabs.sticky", + + # A back-to-top button can be shown when the user, after scrolling down, + # starts to scroll up again. + # https://zensical.org/docs/setup/navigation/#back-to-top-button + "navigation.top", + + # When anchor tracking is enabled, the URL in the address bar is + # automatically updated with the active anchor as highlighted in the table + # of contents. + # https://zensical.org/docs/setup/navigation/#anchor-tracking + "navigation.tracking", + + # When search highlighting is enabled and a user clicks on a search result, + # Zensical will highlight all occurrences after following the link. + # https://zensical.org/docs/setup/search/#search-highlighting + "search.highlight", + + # When anchor following for the table of contents is enabled, the sidebar + # is automatically scrolled so that the active anchor is always visible. + # https://zensical.org/docs/setup/navigation/#anchor-following + # "toc.follow", + + # When navigation integration for the table of contents is enabled, it is + # always rendered as part of the navigation sidebar on the left. + # https://zensical.org/docs/setup/navigation/#navigation-integration + #"toc.integrate", +] + +# ---------------------------------------------------------------------------- +# If you don't have a dedicated project logo, you can use a built-in icon from +# the icon sets shipped in Zensical. Please note that the setting lives in a +# different subsection, and that the above take precedence over the icon. +# +# Read more: +# - https://zensical.org/docs/setup/logo-and-icons +# - https://github.com/zensical/ui/tree/master/dist/.icons +# ---------------------------------------------------------------------------- +[project.theme.icon] +logo = "lucide/utensils" + +# ---------------------------------------------------------------------------- +# In the "palette" subsection you can configure options for the color scheme. +# You can configure different color schemes, e.g., to turn on dark mode, +# that the user can switch between. Each color scheme can be further +# customized. +# +# Read more: +# - https://zensical.org/docs/setup/colors/ +# ---------------------------------------------------------------------------- +[[project.theme.palette]] +scheme = "default" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +scheme = "slate" +toggle.icon = "lucide/moon" +toggle.name = "Switch to light mode" + +# ---------------------------------------------------------------------------- +# The "extra" section contains miscellaneous settings. +# ---------------------------------------------------------------------------- +[[project.extra.social]] +icon = "fontawesome/brands/github" +link = "https://github.com/conorwilliams/libfork" + +# ---------------------------------------------------------------------------- +# In this section you can configure the Markdown extensions that are used when +# rendering your documentation. We enable the most useful extensions by default, +# but you can customize this list to your needs. +# +# Read more: +# - https://zensical.org/docs/setup/extensions/ +# ---------------------------------------------------------------------------- +[project.markdown_extensions.abbr] +[project.markdown_extensions.admonition] +[project.markdown_extensions.attr_list] +[project.markdown_extensions.def_list] +[project.markdown_extensions.footnotes] +[project.markdown_extensions.md_in_html] +[project.markdown_extensions.toc] +permalink = true +[project.markdown_extensions.pymdownx.arithmatex] +generic = true +[project.markdown_extensions.pymdownx.betterem] +[project.markdown_extensions.pymdownx.caret] +[project.markdown_extensions.pymdownx.details] +[project.markdown_extensions.pymdownx.emoji] +emoji_generator = "zensical.extensions.emoji.to_svg" +emoji_index = "zensical.extensions.emoji.twemoji" +[project.markdown_extensions.pymdownx.highlight] +anchor_linenums = true +line_spans = "__span" +pygments_lang_class = true +[project.markdown_extensions.pymdownx.inlinehilite] +[project.markdown_extensions.pymdownx.keys] +[project.markdown_extensions.pymdownx.magiclink] +[project.markdown_extensions.pymdownx.mark] +[project.markdown_extensions.pymdownx.smartsymbols] +[project.markdown_extensions.pymdownx.superfences] +custom_fences = [ + { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" }, +] +[project.markdown_extensions.pymdownx.tabbed] +alternate_style = true +combine_header_slug = true +[project.markdown_extensions.pymdownx.tasklist] +custom_checkbox = true +[project.markdown_extensions.pymdownx.tilde]