How lightweight tasks fit into GNARL.

Flyology changes how a selected task executes without changing its Ada task semantics. Task identity, rendezvous, protected objects, activation, masters, abort, and exception behavior remain within GNARL.

ROUTING

Tasks use either native or lightweight execution.

The integration point is System.Task_Primitives.Operations, below GNARL's language-level task semantics. Task creation captures the designation once and routes the task either to a guarded fiber stack managed by an execution group or to the stock pthread-backed path.

Flyology task routing An Ada task passes through GNARL and task primitives, then follows either the lightweight execution-group lane or the native pthread lane. Application · ordinary Ada task GNARL task semantics Task-primitives routing designation captured at creation Lightweight execution group fiber + scheduler + poller Native task lane stock GNARL pthread path

The environment task is always native. A task cannot change its lightweight or native designation while alive, though a lightweight task can migrate between event-loop groups at an explicit safe point.

SEPARATION

Polling, scheduling, and switching are different jobs.

Keeping the mechanisms separate makes the portability boundary explicit and keeps scheduler policy in Ada.

Event polling
Sleeps until socket readiness, file completion, a timer deadline, or a cross-thread wake. Implemented with kqueue on Darwin and epoll on Linux.
Scheduling
Selects the next runnable fiber from per-priority FIFO buckets and promotes expired per-group deadlines. Implemented as Ada runtime policy.
Context switching
Saves ABI-required register and stack state, then restores another guarded stack. Implemented by a small AArch64 or x86-64 register swap.
LIFECYCLE

A suspended call keeps its stack.

Each lightweight task owns a guarded stackful context. A wait records the task's deadline and ownership state, switches fully back to the scheduler, and later resumes the original call frame.

  1. Ready
  2. Running
  3. Waiting
  4. Ready
  5. Finished
  • Ready to running: the group scheduler dispatches the highest-priority FIFO bucket.
  • Running to waiting: a rendezvous, timer, or I/O operation suspends the fiber.
  • Waiting to ready: GNARL synchronization, a deadline, or descriptor readiness wakes it.
  • Running to migrating: an explicit safe point hands the fully switched-out fiber to another group.

Two loops can never restore one fiber concurrently. The source scheduler performs the migration handoff only after the fiber has completely switched away.

MEMORY

Runtime memory follows task and operation lifetimes.

Flyology does not replace Ada's general heap allocator. It owns the memory needed for lightweight execution: fiber stacks and contexts, scheduler and poller state, and completion records. These resources are created lazily and remain alive while a task or kernel operation can still refer to them.

Fiber stacks
Stacks are mapped in guarded arenas of at most 64 slots, targeting 4 MiB before the final guard. A suspended or migrating task retains the same stack, locals, and exception state.
Guarding and reuse
Each usable stack has an inaccessible guard of at least 64 KiB. A released slot is protected before reuse and receives best-effort page-discard advice; a completely empty arena is unmapped.
Kernel operations
A submitted file buffer remains kernel-owned until completion. Cancellation and abort cannot release the buffer or its completion state early.
Runtime teardown
Finished fibers are reaped when their Ada task lifetime permits it. Groups, pollers, queues, and contexts are released only after GNARL task masters and controlled library objects are complete.

Stack-pool allocation is process-wide and briefly serialized only during task activation and final reap; scheduling, polling, I/O, and context switching do not take that mutex. Flyology.Observability.Stack_Pool reports current arena, stack, usable-byte, and reserved-byte counts without starting an execution group.

TOPOLOGY

Each group is a scheduling and ownership domain.

An execution group owns one stable loop pthread, ready queues, a deadline heap, a descriptor index, and an OS poller. Groups do not steal work implicitly. Automatic placement, explicit Ada CPU selection, and migration determine where a task runs.

Two independent execution groups Each group has its own tasks, priority queues, deadline heap, poller, and stable loop thread. Migration crosses between them explicitly. Group 0 stable loop pthread ready queues deadline heap descriptor index OS poller fibers Group 1 stable loop pthread ready queues deadline heap descriptor index OS poller fibers

Shared identifiers are 0 .. 127. Dedicated identifiers are 128 .. 255. Migrating out of a dedicated group consumes its reservation, so a task must call Create_Dedicated again before re-entering.

SOCKET PATH

Readiness resumes the original operation.

A socket call first attempts the nonblocking operating-system operation. If it would block, the lightweight path registers one-shot readiness and a deadline, then switches back to the scheduler. When resumed, the same call retries the operation.

  1. The Ada task calls receive, send, accept, or connect.
  2. The nonblocking syscall reports that it would block.
  3. The scheduler records descriptor direction, generation, and deadline.
  4. kqueue or epoll reports readiness or the deadline expires.
  5. The task resumes, retries the syscall, and returns a value or exception normally.

Exact reads and complete writes can repeat this cycle while preserving one monotonic deadline. Ownership-aware connections also serialize operations and make cancellation and close descriptor-generation-safe.

FILE PATH

Regular files use kernel completion, not worker threads.

Regular files are usually always reported ready by readiness pollers, so lightweight file I/O uses a completion engine instead.

Darwin
POSIX AIO submissions complete through EVFILT_AIO in the group's kqueue.
Linux primary
Each group owns an io_uring engine integrated with its event loop.
Linux fallback
Linux native AIO completion wakes the group through eventfd.
Native callers
Direct positional syscalls block only the native task's pthread.
RUNTIME BOUNDARY

Target-specific code stays in the C and assembly bridge.

The C and assembly bridge contains Linux syscall numbers, native epoll_event layout, thread placement, virtual-memory operations, test hooks, and ABI register swaps. Scheduler, queue, timeout, backpressure, file-engine, and retry policy remain in Ada.

  • Contexts: AArch64 and x86-64 implementations preserve all ABI-required callee-saved integer, floating-point, stack, frame, and control state.
  • Stacks: guarded mmap arenas detect overflow through the supported GNARL signal path and surface Ada Storage_Error.
  • Patches: exact GNAT source-context patch families fail closed instead of weakening a hunk for an unverified compiler.
  • Finalization: event machinery is one-shot and tears down only after GNARL task masters and controlled library objects are complete.
COMPATIBILITY

Native-only programs start no event machinery.

The environment task is native, undesignated tasks are native by default, and Flyology creates no poller, event-loop thread, scheduler context, or fiber stack until the first lightweight task is activated.

This is a testable contract, not only a configuration preference. The external-consumer suite prepares both native-default and lightweight-default runtimes, then checks that an ordinary native-only program has no event machinery before or after creating a normal Ada task.