Landin library reference discuss source

Shared: available on every enabled target.

Fibres: tasks a routine starts as a group and waits for before it goes on.

Call What it does
fibre.group_over Cut a backing the caller lends into equal stacks, run by any fibre.scheduler, and return the group from both.
fibre.turns_over The same over a fibre.turns, for tasks that must take turns.
fibre.admit Wait for a free stack, copy a state onto it and start a task that calls a routine with the copy.
fibre.join Wait until every task has ended, and fail with the first failure any had.
fibre.yield Let the provider's other fibres run, through turns evidence.

A task is an ordinary routine, (inout state: T) -> none ! failure.reason, and its state a value copied onto its own stack, so one group holds tasks of different state types. The state is kept until the task ends, escaping into the group: what it refers to must outlive the group, as the routine's own parameters and the locals declared before the group do.

Two concepts say what runs the tasks:

Concept What it promises Providers
scheduler start, finished and wait; it may run each task to its end as it is admitted fibre.whole, the blocking host, and every provider of turns
turns a scheduler with yield: a task that yields lets every other unfinished one run first stacks.round
  • A program written against scheduler must be right when every task runs to its end. One that needs turns asks turns_over, and is refused there for a provider of scheduler alone.
  • Fibres are cooperative: a fibre gives the core away only inside a call to its provider, and the fibres of one provider are one execution context.
  • The first failure is kept and the rest are dropped; nothing cancels a fibre, so join returns only once every task has ended.
  • A group must be joined before its backing goes. Nothing but the program's defer fibre.join(group) else none makes sure it is.
  • Each stack is at least 512 bytes after its record; a smaller backing fails mem.out_of_memory.

Items

Concepts 3

Types 3

Also on this page

public scheduler: type = concept (provider: type)

Start work on record and stack, or run it to its end.

    start: (self: provider, escaping record: ptr mut space,
            escaping stack: []mut u8, escaping work: any task)
           -> none ! mem.out_of_memory

Whether the task last started on record has ended.

    finished: (self: provider, record: space) -> (yes: bool)

Wait until one of the provider's fibres has finished.

    wait: (self: provider) -> none
end scheduler

What runs fibres. A scheduler may run a task to its end inside start, before returning, and a program written against it must be right when every task does. start fails mem.out_of_memory when stack cannot hold the task's first frame. finished says whether the task last started on record has ended. wait returns once one of the provider's fibres has finished, or at once when none is running, which is how a group waits for a free stack. The provider runs at most one of its fibres at a time, and its fibres are one execution context (D446).

public task: type = concept (task_type: type)

Run the task to its end.

    run: (self: ptr mut task_type) -> none
end task

What a provider starts: one entry, no error set, since a fibre's first frame has no caller to fail to. An application does not write one; a group makes it from a routine and a state.

public turns: type = concept (provider: type) is scheduler

Let every other unfinished fibre run, then go on.

    yield: (self: provider) -> none
end turns

A scheduler whose fibres take turns. yield gives the core to another fibre of the provider that can run, and its contract is the promise: a fibre that yields lets every other unfinished fibre of the provider run before it resumes. A provider that runs every task to its end has nothing to yield to, and so does not provide turns; a program that needs turns asks for them with turns_over, and is refused there.

public group: type = group_value

Fibres a routine starts and waits for: count equal stacks cut from a backing the caller lends, each with its record at its base, and the provider that runs them. Made by group_over or turns_over, given tasks by admit, and waited for by join, which the routine making the group defers as it would a region's release: a group must be joined before its backing goes, and nothing else makes sure it is.

public space: type = struct
    saved: usize
    status: u8
end space

The caller's record of one fibre: where its stack pointer was saved, and whether a task runs on it. A group keeps these at the base of the stacks it cuts, so an application never declares one. status is zero when no task runs on the record, before the first start and after a task has ended; a provider makes it nonzero while one does. Where the stack ends is in the frames the switch saves, not here (D472).

public whole: type = struct
    reserved: u32
end whole

A provider that has no second stack to give: it runs every task to its end, on the caller's stack, before start returns, and ignores the stack it is given. The blocking host, system.host, is another; a device that wants no switching is a third. Make one zeroed.

public admit: (state_type: type, inout g: group,
               work: (inout state: state_type) -> none ! failure.reason,
               escaping into g state: state_type)
              -> none ! mem.out_of_memory

Start a task: wait, through the provider, until one of the group's stacks is free; copy state onto the top of that stack, as a call passes an argument; and start a fibre whose first frame calls work with the copy. Because each task's state lives on its own stack, one group holds tasks of different state types.

work's failure is kept by the group, the first one only, and join fails with it. A task with atoms of its own maps them to failure.reason before returning. state is kept in the group until the task ends, so what it refers to must outlive the group (D445): a parameter of the routine making the group does, and so does a local declared before the group. It fails mem.out_of_memory when the state leaves the stack less than its least, 512 bytes, for its own calls.

group_over

function core/fibre/fibre.ldn:204 ↑ index
public group_over: (runs: any scheduler, backing: []mut u8, count: usize)
                   -> (g: group from runs, backing) ! mem.out_of_memory

A group over a scheduler, which may run each task to its end as it is admitted. It cuts backing into count equal stacks and fails mem.out_of_memory when they would be smaller than a stack's least, 512 bytes after its record. The group borrows the provider and the backing for as long as it is used [0790].

public join: (inout g: group) -> none ! failure.reason

Wait until every task admitted to g has ended. The first failure any of them had is join's, handed on once; the rest ran to their own ends regardless, since nothing cancels a fibre (D449). A group may be joined again, and given more tasks after it is.

turns_over

function core/fibre/fibre.ldn:215 ↑ index
public turns_over: (runs: any turns, backing: []mut u8, count: usize)
                   -> (g: group from runs, backing) ! mem.out_of_memory

A group over a provider of turns, whose tasks take turns: a task that yields, or waits in a call to the provider, lets the others run. A provider that cannot promise that is refused here, where the group is made, rather than when a task waits for ever.

public yield: (provider: type is turns, host: provider) -> none

Give the core to the provider's other fibres, through turns evidence, for a provider the program holds as itself.

Example program

This complete program is one of the compiler's runtime tests. View source.

compiler/tests/fixtures/runtime/fibre-same-source-any-provider/main.ldn130 lines
--  D444, prototype 5's two requests in miniature: one task waits on a
--  clock and the other answers at once, from one source.  Over the
--  blocking host and over fibre.whole each task runs to its end as it is
--  admitted, so `a` is answered first; over a round of turns, whose clock
--  takes turns while it waits, `b` is answered while `a` waits.  Each task
--  records its answer through a pointer to the caller's parameter, which
--  the group keeps through `escaping into` (D445).
import core/failure
import core/fibre
import core/fibre/stacks
import core/io
import core/time
import hosted/system

answers: type = struct
    order: [2]u8
    count: usize
end answers

asking: type = struct
    clock: any time.clock
    wait: u64
    tag: u8
    heard: ptr mut answers
end asking

--  Nothing in it knows it runs on a fibre.
ask: (inout self: asking) -> none ! failure.reason =
    if self.wait > 0 then
        self.clock.sleep(time.milliseconds(self.wait))
    end if
    self.heard.val.order[self.heard.val.count] = self.tag
    self.heard.val.count += 1
end ask

ask_both: (runs: any fibre.scheduler, clock: any time.clock,
           inout heard: answers) -> none ! failure.reason =
    --  A task here takes about 4.4 KiB of its stack on RV64, whose
    --  frames keep every scalar in a home of their own, at every profile.
    mut stacks: [2 * 8192]u8 = zeroed
    mut asks := try fibre.group_over(runs, stacks, 2)
    defer fibre.join(asks) else none
    a: asking = (clock: clock, wait: 20, tag: u8('a'), heard: addr heard)
    try fibre.admit(asks, ask, a)
    b: asking = (clock: clock, wait: 0, tag: u8('b'), heard: addr heard)
    try fibre.admit(asks, ask, b)
    try fibre.join(asks)
end ask_both

--  A clock whose sleep returns at once, having moved its reading on.
still: type = struct
    reading: ptr mut u64
end still

still_now: (self: still) -> (at: time.instant) =
    at = time.instant(self.reading.val)
end still_now

still_sleep: (self: still, span: time.duration) -> none =
    self.reading.val += u64(span)
end still_sleep

still is time.clock (now: still_now, sleep: still_sleep)

--  A clock that takes a turn for each millisecond it sleeps: its reading
--  moves on only while its sleeper waits, and the round's other fibres
--  run meanwhile.
turning: type = struct
    reading: ptr mut u64
    runs: any fibre.turns
end turning

turning_now: (self: turning) -> (at: time.instant) =
    at = time.instant(self.reading.val)
end turning_now

turning_sleep: (self: turning, span: time.duration) -> none =
    deadline: u64 = self.reading.val + u64(span)
    while self.reading.val < deadline do
        self.runs.yield()
        self.reading.val += u64(time.milliseconds(1))
    end while
end turning_sleep

turning is time.clock (now: turning_now, sleep: turning_sleep)

show: (provider: type is io.writer, inout out: io.buffered_writer(provider),
       name: []u8, heard: answers) -> none ! io.io_failed =
    try io.put(out, name)
    try io.put(out, ": ")
    try io.put(out, heard.order[0..<heard.count])
    try io.put(out, "\n")
end show

--  The same `ask_both` over three providers.
run: (host: system.host) -> none ! failure.reason =
    mut storage: [64]u8 = zeroed
    mut out := io.buffered_over(host, io.out(host), storage)
    defer io.flush(out) else none

    mut over_host: answers = zeroed
    try ask_both(any(addr host), any(addr host), over_host)
    try show(out, "host", over_host)

    runner: fibre.whole = zeroed
    mut reading: u64 = 0
    stopped: still = (reading: addr reading)
    mut over_whole: answers = zeroed
    try ask_both(any(addr runner), any(addr stopped), over_whole)
    try show(out, "whole", over_whole)

    mut state: stacks.round_space = zeroed
    mut entries: [3]usize = zeroed
    idler: stacks.spin = zeroed
    round := try stacks.round_over(state, entries, any(addr idler))
    taking: turning = (reading: addr reading, runs: any(addr round))
    mut over_round: answers = zeroed
    try ask_both(any(addr round), any(addr taking), over_round)
    try show(out, "round", over_round)
end run

public main: () -> (code: i32) =
    code = 1
    host := system.new()
    run(host) else (problem)
        _ = problem
        return
    end
    code = 0
end main
in a terminalprints on standard output
$ refine compile main.ldn -o fibre-same-source-any-provider && ./fibre-same-source-any-provider
host: ab
whole: ab
round: ba