Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ target_sources(libfork_libfork
src/batteries/geometric_stack.cxx
src/batteries/adaptor_stack.cxx
src/batteries/slab_stack.cxx
src/batteries/dummy_stack.cxx
# libfork.schedulers
src/schedulers/schedulers.cxx
src/schedulers/inline.cxx
Expand Down
8 changes: 4 additions & 4 deletions benchmark/lib/macros.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ inline auto inverse_complexity(benchmark::IterationCount n) -> double { return 1
inline void setup_single(benchmark::Benchmark *b, std::int64_t size) { b->Arg(size)->UseRealTime(); }

inline void setup_mt(benchmark::Benchmark *b, std::int64_t size) {
b->Apply([size](benchmark::Benchmark *b) {
bench_thread_args(b, [size](benchmark::Benchmark *inner_b, unsigned t) {
b->Apply([size](benchmark::Benchmark *bm) {
bench_thread_args(bm, [size](benchmark::Benchmark *inner_b, unsigned t) {
inner_b->Args({size, static_cast<std::int64_t>(t)});
});
})
Expand All @@ -46,8 +46,8 @@ inline void setup_mt(benchmark::Benchmark *b, std::int64_t size) {
}

inline void setup_uts_mt(benchmark::Benchmark *b) {
b->Apply([](benchmark::Benchmark *b) {
bench_thread_args(b, [](benchmark::Benchmark *inner_b, unsigned t) {
b->Apply([](benchmark::Benchmark *bm) {
bench_thread_args(bm, [](benchmark::Benchmark *inner_b, unsigned t) {
inner_b->Arg(static_cast<std::int64_t>(t));
});
})
Expand Down
3 changes: 2 additions & 1 deletion benchmark/src/baremetal/fib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ namespace {

[[nodiscard]]
inline auto fib_align_size(std::size_t n) -> std::size_t {
return (n + __STDCPP_DEFAULT_NEW_ALIGNMENT__ - 1) & ~(__STDCPP_DEFAULT_NEW_ALIGNMENT__ - 1);
constexpr std::size_t align = __STDCPP_DEFAULT_NEW_ALIGNMENT__;
return (n + align - 1) & ~(align - 1);
}

constinit inline thread_local std::byte *tls_bump_ptr = nullptr;
Expand Down
31 changes: 12 additions & 19 deletions benchmark/src/libfork/fib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,41 +62,34 @@ void run(benchmark::State &state) {

} // namespace

template <typename Stack, typename Adaptor>
using real_context = lf::mono_context<Stack, Adaptor>;

template <typename Stack, typename Adaptor>
using poly_context = lf::derived_poly_context<Stack, Adaptor>;

using lf::adapt_deque;
using lf::adapt_vector;
using lf::inline_scheduler;

using lf::adaptor_stack;
using lf::geometric_stack;
using lf::slab_stack;

// -- Vector

LIBFORK_BENCH_ALL(run, fib, fib, inline_scheduler<real_context<adaptor_stack<>, adapt_vector<>>>)
LIBFORK_BENCH_ALL(run, fib, fib, inline_scheduler<poly_context<adaptor_stack<>, adapt_vector<>>>)
LIBFORK_BENCH_ALL(run, fib, fib, lf::mono_inline_scheduler<adaptor_stack<>, adapt_vector<>>)
LIBFORK_BENCH_ALL(run, fib, fib, lf::poly_inline_scheduler<adaptor_stack<>, adapt_vector<>>)

LIBFORK_BENCH_ALL(run, fib, fib, inline_scheduler<real_context<slab_stack<>, adapt_vector<>>>)
LIBFORK_BENCH_ALL(run, fib, fib, inline_scheduler<poly_context<slab_stack<>, adapt_vector<>>>)
LIBFORK_BENCH_ALL(run, fib, fib, lf::mono_inline_scheduler<slab_stack<>, adapt_vector<>>)
LIBFORK_BENCH_ALL(run, fib, fib, lf::poly_inline_scheduler<slab_stack<>, adapt_vector<>>)

LIBFORK_BENCH_ALL(run, fib, fib, inline_scheduler<real_context<geometric_stack<>, adapt_vector<>>>)
LIBFORK_BENCH_ALL(run, fib, fib, inline_scheduler<poly_context<geometric_stack<>, adapt_vector<>>>)
LIBFORK_BENCH_ALL(run, fib, fib, lf::mono_inline_scheduler<geometric_stack<>, adapt_vector<>>)
LIBFORK_BENCH_ALL(run, fib, fib, lf::poly_inline_scheduler<geometric_stack<>, adapt_vector<>>)

// -- Deque

LIBFORK_BENCH_ALL(run, fib, fib, inline_scheduler<real_context<adaptor_stack<>, adapt_deque<>>>)
LIBFORK_BENCH_ALL(run, fib, fib, inline_scheduler<poly_context<adaptor_stack<>, adapt_deque<>>>)
LIBFORK_BENCH_ALL(run, fib, fib, lf::mono_inline_scheduler<adaptor_stack<>, adapt_deque<>>)
LIBFORK_BENCH_ALL(run, fib, fib, lf::poly_inline_scheduler<adaptor_stack<>, adapt_deque<>>)

LIBFORK_BENCH_ALL(run, fib, fib, inline_scheduler<real_context<slab_stack<>, adapt_deque<>>>)
LIBFORK_BENCH_ALL(run, fib, fib, inline_scheduler<poly_context<slab_stack<>, adapt_deque<>>>)
LIBFORK_BENCH_ALL(run, fib, fib, lf::mono_inline_scheduler<slab_stack<>, adapt_deque<>>)
LIBFORK_BENCH_ALL(run, fib, fib, lf::poly_inline_scheduler<slab_stack<>, adapt_deque<>>)

LIBFORK_BENCH_ALL(run, fib, fib, inline_scheduler<real_context<geometric_stack<>, adapt_deque<>>>)
LIBFORK_BENCH_ALL(run, fib, fib, inline_scheduler<poly_context<geometric_stack<>, adapt_deque<>>>)
LIBFORK_BENCH_ALL(run, fib, fib, lf::mono_inline_scheduler<geometric_stack<>, adapt_deque<>>)
LIBFORK_BENCH_ALL(run, fib, fib, lf::poly_inline_scheduler<geometric_stack<>, adapt_deque<>>)

LIBFORK_BENCH_ALL_MT(run, fib, fib, mono_busy_pool)
LIBFORK_BENCH_ALL_MT(run, fib, fib, poly_busy_pool)
287 changes: 287 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
# libfork Public API

All symbols live in the `lf` namespace. Access them via `import libfork;`.

---

## Core concepts

### `concept returnable<T>` — `:task`

`T` is `void` or a `std::movable` plain object type. Used as the return-type constraint on async functions.

### `concept worker_stack<T>` — `:concepts_stack`

A type that provides a contiguous stack with `push`, `pop`, `checkpoint`, `prepare_release`, `release`, and `acquire`.

### `concept lifo_stack<T, U>` — `:concepts_context`

`T` is a plain object type supporting `push(U)` and a `noexcept pop() -> U`. Used to define `worker_context`.

### `concept worker_context<T>` — `:concepts_context`

A type that satisfies `lifo_stack<T, steal_handle<T>>` and exposes a `worker_stack` via a `noexcept stack()`.

### `using stack_t<Context>` — `:concepts_context`

Extracts the stack type from a `worker_context`.

### `concept has_context_typedef<T>` — `:concepts_scheduler`

`T` has a `context_type` member typedef. Used to define `scheduler` and constrain `context_t`.

### `concept scheduler<Sch>` — `:concepts_scheduler`

A type satisfying `has_context_typedef` with a `post(sched_handle<context_type>)` method.

### `using context_t<T>` — `:concepts_scheduler`

Extracts `T::context_type`. Requires `has_context_typedef<T>`.

### `concept async_invocable<Fn, Context, Args...>` — `:concepts_invocable`

`Fn` is callable with an `env<Context>` (or without one) and `Args...`, returning an `lf::task`.

### `concept async_nothrow_invocable<Fn, Context, Args...>` — `:concepts_invocable`

Subsumes `async_invocable` and requires the call to be `noexcept`.

### `using async_result_t<Fn, Context, Args...>` — `:concepts_invocable`

The `value_type` of the `task` returned by invoking `Fn`.

### `concept async_invocable_to<Fn, R, Context, Args...>` — `:concepts_invocable`

Subsumes `async_invocable` and constrains the result type to `R`.

### `concept async_nothrow_invocable_to<Fn, R, Context, Args...>` — `:concepts_invocable`

Subsumes both `async_nothrow_invocable` and `async_invocable_to`.

---

## Coroutine types

### `struct env<Context>` — `: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<T, Context>` — `: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<Context>` — `: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<Context>` — `: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<Context>` 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<Context>`, 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<T, Stoppable = false>` — `:receiver`

Pre-allocated shared state for a root task. Constructors mirror `make_shared` / `allocate_shared`:

```cpp
recv_state<int> s; // default-init
recv_state<int> s{42}; // in-place init
recv_state<int> s{std::allocator_arg, alloc}; // custom allocator
recv_state<int> s{std::allocator_arg, alloc, 42}; // custom allocator + in-place init
recv_state<int, true> s; // cancellable variant
```

Move-only. Pass to `schedule()` to get back a `receiver`.

### `class receiver<T, Stoppable = false>` — `: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<R,S>, Fn&&, Args&&...) -> receiver<R,S>` — `:schedule`

Schedule an async function as a root task using a pre-allocated `recv_state`.

### `auto schedule(Sch&&, Fn&&, Args&&...) -> receiver<R>` — `:schedule`

Convenience overload: default-constructs a non-cancellable `recv_state<R>`.

### `void execute(Context&, sched_handle<Context>)` — `:execute`

Bind the calling thread to `context` and resume the scheduled task. Used by scheduler implementations.

### `void execute(Context&, steal_handle<Context>)` — `:execute`

Bind the calling thread to `context` and resume a stolen task. Used by scheduler implementations.

---

## Polymorphic context base classes

### `class base_context<Stack>` — `:poly_context`

CRTP base providing `stack()` -> `Stack&`. Inherit from this (or `poly_context`) when implementing a custom context.

### `class poly_context<Stack>` — `:poly_context`

Abstract base for polymorphic contexts. Provides pure-virtual `push(steal_handle<poly_context>)`, `pop()`, and a defaulting `post(sched_handle<poly_context>)` 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<Allocator>` — `:geometric_stack`

Segmented stack with geometric growth and segment caching. Recommended default.

### `class adaptor_stack<Allocator>` — `:adaptor_stack`

Thin allocator-backed stack; allocates/deallocates on every push/pop.

### `class slab_stack<Allocator>` — `:slab_stack`

Fixed-capacity slab stack; throws on overflow.

---

## Batteries: deque and adaptors

### `class deque<T, Allocator>` — `:deque`

Lock-free Chase-Lev work-stealing deque. `T` must be `lock_free` and `default_initializable`. Methods: `push(T)`, `pop() -> std::optional<T>`, `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<T>`.

### `enum class err` — `:deque`

Return code from low-level steal operations: `none`, `lost`, `empty`.

### `struct steal_t<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<Allocator>` — `:adaptors`

`std::vector`-backed LIFO deque policy. Satisfies `deque_policy`.

### `class adapt_deque<Allocator>` — `:adaptors`

Lock-free deque-backed policy. Satisfies both `deque_policy` and `stealable_deque_policy`.

---

## Batteries: context policies and contexts

### `concept deque_policy<T>` — `:contexts`

A type that is a LIFO stack over `unsafe_steal_handle` (has `push` and `pop`).

### `concept stealable_deque_policy<T>` — `:contexts`

Extends `deque_policy` with a `steal() -> unsafe_steal_handle` method for FIFO work stealing.

### `class mono_context<Stack, Deque>` — `:contexts`

Monomorphic worker context. Composes a `worker_stack` and a `deque_policy`. Satisfies `worker_context<mono_context>`. Exposes `steal()` when `Deque` satisfies `stealable_deque_policy`.

### `class derived_poly_context<Stack, Deque>` — `:contexts`

Polymorphic worker context. Derives from `poly_context<Stack>` and implements `push`/`pop` via `Deque`. Exposes `steal()` when `Deque` satisfies `stealable_deque_policy`. The `context_type` alias is `poly_context<Stack>`.

---

## Schedulers

### `concept derived_worker_context<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<Context>` — `:inline_scheduler`

Single-threaded synchronous scheduler. Stores one `Context` instance; `post()` calls `execute()` directly on the calling thread.

Comment on lines +269 to +272
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether mono/poly inline scheduler aliases exist in the codebase
# and compare with docs coverage.

set -euo pipefail

echo "== Scheduler symbol definitions/usages =="
rg -n -C2 --iglob '*.{hpp,h,hh,hxx,ipp,cpp,cc,cxx,ixx,cppm,md}' \
  '\b(mono_inline_scheduler|poly_inline_scheduler|inline_scheduler)\b'

echo
echo "== docs/api.md scheduler section =="
rg -n -C3 '\b(mono_inline_scheduler|poly_inline_scheduler|inline_scheduler)\b' docs/api.md

Repository: ConorWilliams/libfork

Length of output: 5391


Document the new inline scheduler aliases in the API reference.

Lines 269–272 document inline_scheduler<Context>, but the PR exports two new public template aliases (mono_inline_scheduler and poly_inline_scheduler) that are not listed in the API docs. These aliases provide convenient default context configurations and are actively used in tests and benchmarks. Add them to this section alongside the base template to ensure the documentation matches the exported API surface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/api.md` around lines 269 - 272, The API docs list
inline_scheduler<Context> but omit the two new public template aliases
mono_inline_scheduler and poly_inline_scheduler; update the docs in the inline
scheduler section to mention both aliases alongside inline_scheduler<Context>,
describe that they are template aliases providing convenient default context
configurations (used in tests/benchmarks), and ensure their names and short
descriptions appear in the same format as the existing entry so the documented
API surface matches the exported symbols mono_inline_scheduler and
poly_inline_scheduler.

### `enum class pool_kind` — `:basic_busy_pool`

`mono` — uses `mono_context`; `poly` — uses `derived_poly_context`.

### `class basic_busy_pool<Kind, Stack, Deque, Alloc>` — `: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<Stack, Deque, Alloc>` — `:basic_busy_pool`

Alias for `basic_busy_pool<pool_kind::mono, ...>`.

### `using poly_busy_pool<Stack, Deque, Alloc>` — `:basic_busy_pool`

Alias for `basic_busy_pool<pool_kind::poly, ...>`.
1 change: 0 additions & 1 deletion src/batteries/batteries.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@ export import :deque;
export import :geometric_stack;
export import :adaptor_stack;
export import :slab_stack;
export import :dummy_stack;
export import :adaptors;
export import :contexts;
Loading
Loading