Silk

Single-threaded schedulers and Fibers

The silk.scheduler, silk.fiber, and silk.local_scheduler modules provide explicit local structured concurrency in ordinary Silk source. An application selects a scheduler provider at its entry point. The provider owns the root and every child as independently resumable Executions until the root terminates.

These APIs build on independently resumable Effect executions. The compiler owns the target-neutral Execution and Wake lifecycle. Scheduler selection, ready order, task storage, Fiber observation, and structured cancellation remain standard-library policy.

Terms

  • A Scheduler provider prepares child tasks and owns the policy that drives them.
  • The root is the first scheduler-owned task created for one execute call.
  • A Fiber is one affine authority to observe a child's success, typed failure, or cancellation.
  • Publication is the atomic transfer of one fully prepared child into scheduler-owned storage.
  • A ready task is stored and eligible for a later drive. Readiness does not run its body inline.
  • Structured cancellation releases an unfinished task because its parent has terminated.

Enter a local scheduler explicitly

LocalScheduler.execute receives the program before it starts, creates task zero, provides an owned Scheduler client, and privately drives the ready queue until the root terminates.

import silk.allocator { OutOfMemoryError }
import silk.effect { Effect }
import silk.fiber { Fiber }
import silk.local_scheduler { LocalScheduler }
import silk.scheduler { Scheduler }

effect fn work() -> i32 {
  return 42
}

effect fn program() -> i32
! OutOfMemoryError | Scheduler.TaskIdExhaustedError | Fiber.Cancelled
? &mut Scheduler.Scheduler {
  let child = run Fiber.forkChild<i32, never>(work())
  return run Fiber.join<i32, never>(move child)
}

effect fn recover(
  error: OutOfMemoryError
    | Scheduler.TaskIdExhaustedError
    | Fiber.Cancelled
    | LocalScheduler.StalledError,
) -> i32 {
  drop error
  return -1
}

pub fn main() -> i32 {
  let mut scheduler = LocalScheduler.make()
  return run Effect.catchAll(
    LocalScheduler.execute(&mut scheduler, program()),
    recover,
  )
}

Application code inside program uses Fiber operations. It does not call a public scheduler loop. A different provider may expose its own explicit entry operation while implementing the same ordinary Scheduler preparation service.

FIBER-001 — Applications select scheduler entry explicitly

Status: Confirmed

An application constructs its provider and calls that provider's entry operation. For LocalScheduler, the operation is execute. The lazy root program does not begin before execute stores it as task zero and reports its initial readiness.

The root has the same homogeneous Execution<()> task shape, mailbox, ready endpoint, and structured-lifetime links as its children. Its generic success or typed failure is stored separately and extracted only after scheduler shutdown.

Boundary: Silk does not select a scheduler at main, create an implicit root owner, or infer a Scheduler requirement merely because code can park. A park-capable entry without an explicit Execution owner remains invalid.

Diagnostics: A park-capable Effect with no explicit owner reports SEM0140. Scheduler choice and construction have no compiler diagnostic because they are ordinary source calls.

Evidence: LocalScheduler source, scheduler and Fiber tests.

FIBER-002 — Child creation is atomic and initially deferred

Status: Confirmed

Fiber.forkChild accepts a consuming lazy Effect whose remaining runtime requirement is at most the child's owned Scheduler provider. Closed work is therefore valid. The selected provider funds the completion cells, child client, mailbox, ready endpoint, identity, and Execution before the child can become observable.

The parent parks while the driver inserts the prepared task. On success, the driver publishes the response and notifies the parent first. It then reports the stored child's initial readiness. Deterministic FIFO order lets forkChild return the Fiber before the child's first body activation.

Boundary: Preparation failure or task-store insertion refusal returns no Fiber and leaves no runnable child. Task identity exhaustion raises Scheduler.TaskIdExhaustedError; allocation refusal raises Allocator.OutOfMemoryError. Neither failure is converted to a child outcome.

Diagnostics: A child Effect with additional unresolved runtime requirements fails ordinary Effect-contract compatibility. No declaration receives special behavior from the spelling Scheduler, Fiber, or forkChild.

Evidence: Scheduler preparation protocol, atomic fork fixture.

FIBER-003 — Fiber observation is affine and non-polling

Status: Confirmed

Fiber.await consumes one Fiber<A, E> and returns Fiber.Outcome<A, E> containing success, typed failure, or cancellation. Fiber.join consumes the same authority, returns A on success, and raises either the original E or Fiber.Cancelled.

Observation of a terminal Fiber completes immediately. Observation of an incomplete Fiber parks the current Execution and installs at most one Wake. Completion and resumption allocate no storage, and the affine handle makes a second observation unavailable.

Boundary: Dropping the Fiber abandons observation only. It neither detaches nor immediately cancels the child. The task remains attached to its parent until either one terminates.

Diagnostics: Reusing a Fiber consumed by await or join reports the ordinary affine OWN0001 diagnostic.

Evidence: Fiber source, observation fixture.

FIBER-004 — Child lifetime is structured by the parent task

Status: Confirmed

Every child remains linked to the task that created it. Parent success, typed failure, cancellation, or stalled shutdown cancels and releases every unfinished descendant before the driver dispatches another task that can observe the parent's terminal outcome.

Cancellation publishes Fiber.Cancelled through the type-erased completion signal and destroys the child's Execution. A Fiber returned through its terminating parent's result remains a valid observer, but it observes cancellation when the child was unfinished.

Boundary: The initial API has no detached task, daemon task, reparenting, or public interrupt operation. Returning or dropping a handle does not change the task tree.

Diagnostics: Structured cancellation is a scheduler transition, not a source diagnostic and not a fabricated child failure value.

Evidence: LocalScheduler shutdown fixture, structured shutdown tests.

FIBER-005 — Local ready order is deterministic FIFO

Status: Confirmed

LocalScheduler appends each readiness notification to one FIFO queue. A task owns one preallocated intrusive ready node, so Wake notification and Fiber.yieldNow do not allocate. Duplicate readiness is suppressed until the selected node leaves the queue.

Fiber.yieldNow parks and immediately consumes its Wake. Tasks already ready remain ahead of the yielding task. With no competitor, the same task becomes the next eligible selection.

Boundary: FIFO describes readiness selection, not parallel fairness or preemption. A task runs until it completes, parks, or yields. A stale ready node for a removed task is discarded and cannot name a task from a later execute call.

Diagnostics: Ready order and stale-node discard do not add typed failures or source diagnostics.

Evidence: LocalScheduler ready queue, FIFO, yield, and stale-readiness tests.

FIBER-006 — Typed shutdown preserves the root outcome and provider reuse

Status: Confirmed

LocalScheduler.execute returns the root's exact success value or raises its exact typed failure. If no task is ready while the root remains incomplete, it cancels the remaining task tree and raises LocalScheduler.StalledError. Setup or task-store allocation refusal raises Allocator.OutOfMemoryError. Fatal traps remain outside typed recovery.

Before any typed return or failure, shutdown removes every task, drains prepared submissions, releases scheduler-owned handles, and drops the per-run ready queue. The same LocalScheduler value can then execute another program with fresh task identities and readiness state.

Boundary: A retained cancelled Wake may delay release of its old compiler-owned package allocation. It is inert and cannot enqueue into a later run.

Diagnostics: Root typed failures and scheduler failures use their declared Effect channels. Fatal lifecycle or intrinsic-state errors remain fatal traps.

Evidence: generic root and reuse tests, fiber capability specification.

FIBER-007 — Scheduler policy remains ordinary source

Status: Confirmed

Scheduler, Fiber, LocalScheduler, task storage, completion, FIFO readiness, and structured cancellation are navigable Silk declarations. A renamed provider implementing the same service contract receives the same analysis, ownership, lowering, and execution behavior.

The compiler recognizes only the sealed target-neutral Execution, Wake, parking, and initial readiness operations. Execution.notifyInitial changes a stored Initial Execution to InitialReady and invokes its fixed endpoint; it does not know which queue or scheduling policy that endpoint implements.

Boundary: Importing no Fiber or scheduler module adds no service, task store, queue, Execution, or Wake machinery to a trivial program.

Diagnostics: No diagnostic applies to a legal renamed provider. Unsupported Execution behavior is reported through the existing target-availability and lifecycle boundaries rather than a Scheduler-specific compiler branch.

Evidence: independent-execution boundary, renamed provider fixture.

On this page