Landin library reference discuss source

Shared: available on every enabled target.

A cell holding one u8, u16, u32 or u64, read and written only atomically, its operations' orderings in their names.

Call What it does
atomic.new Make a cell holding a value, for a local or a field; module storage starts one zeroed.
atomic.load, atomic.store Read with acquire ordering, write with release ordering.
atomic.load_relaxed, atomic.store_relaxed The same, ordering nothing else: for a cache or a count.
atomic.sole_add Add as the cell's one writer, wrapping, and return what it held: a relaxed load and a release store.
atomic.exchange Store a value and return what the cell held, in one step that acquires and releases.
atomic.add, atomic.add_relaxed Add, wrapping, and return what the cell held, in one step; the relaxed form orders nothing else.
atomic.compare_exchange Store a value if the cell holds the one expected, and return what it held: equal to the expected exactly when it stored.

Every operation takes the cell's address, addr cell, so a cell lives in module storage or a field others reach by pointer, and is never copied while another context uses it. Sequential consistency is not offered; the compiler's builtins have it ([1620]).

sole_add is correct only while one context alone writes the cell, an interrupt handler or one thread, since a second writer's store between its load and its store is lost; it needs no read-modify-write, so every target has it. Prototype 5's tick is one: the handler counts with it and any reader loads.

mut ticks: atomic.cell(u32) = zeroed

count_tick: () -> none =
    _ = atomic.sole_add(addr ticks, 1)
end count_tick

now_ticks: () -> (count: u32) =
    count = atomic.load(addr ticks)
end now_ticks

Cortex-M0 has no atomic read-modify-write and no eight-byte atomic access. There a u64 cell, exchange, add, add_relaxed and compare_exchange are refused with L0344 where the program writes them, the refusal naming sole_add and platform/cortex_m's critical section, disable_interrupts to restore_interrupts, which state several contexts write takes instead. A cell of any other type is refused on every target.

Items

exchangeable

concept core/atomic/atomic.ldn:38 ↑ index
public exchangeable: type = concept (item: type) is scalar
    exchange: (cell: ptr mut item, value: item) -> (previous: item)
    add: (cell: ptr mut item, value: item) -> (previous: item)
    add_relaxed: (cell: ptr mut item, value: item) -> (previous: item)
    compare_exchange: (cell: ptr mut item, expected: item, desired: item)
                      -> (observed: item)
end exchangeable

The scalars whose cells also take a read-modify-write: each of u8, u16, u32 and u64 where the target has atomic read-modify-write, which Cortex-M0 has not.

public scalar: type = concept (item: type)
    load: (cell: ptr item) -> (value: item)
    load_relaxed: (cell: ptr item) -> (value: item)
    store: (cell: ptr mut item, value: item) -> none
    store_relaxed: (cell: ptr mut item, value: item) -> none
    sole_add: (cell: ptr mut item, value: item) -> (previous: item)
end scalar

The scalars a cell can hold, with the accesses every target admits for them: u8, u16 and u32 everywhere, and u64 where the target has eight-byte atomics, which Cortex-M0 has not. The entries are the cell's operations' own, over the scalar's address; use the operations below.

public cell: type (item: type is scalar) = cell_value(item)

One u8, u16, u32 or u64 read and written atomically. A zeroed cell holds zero.

public add: (item: type is exchangeable, at: ptr mut cell(item), value: item)
            -> (previous: item)

Add value to at, wrapping, and return what it held, in one indivisible step that acquires and releases. Refused on Cortex-M0.

add_relaxed

function core/atomic/atomic.ldn:269 ↑ index
public add_relaxed: (item: type is exchangeable, at: ptr mut cell(item),
                     value: item) -> (previous: item)

Add value to at, wrapping, and return what it held, in one indivisible step with no ordering beyond the cell's own: a counter several contexts bump. Refused on Cortex-M0.

compare_exchange

function core/atomic/atomic.ldn:279 ↑ index
public compare_exchange: (item: type is exchangeable, at: ptr mut cell(item),
                          expected: item, desired: item)
                         -> (observed: item)

Store desired into at if it holds expected, and return what it held, in one indivisible step: the store happened exactly when the result equals expected, and never fails spuriously. A store acquires and releases; a comparison that fails only acquires. Refused on Cortex-M0.

public exchange: (item: type is exchangeable, at: ptr mut cell(item),
                  value: item) -> (previous: item)

Store value into at and return what it held, in one indivisible step that acquires and releases. Refused on Cortex-M0.

public load: (item: type is scalar, at: ptr cell(item)) -> (value: item)

The value at holds, with acquire ordering: what the store that wrote it released is visible after it.

load_relaxed

function core/atomic/atomic.ldn:222 ↑ index
public load_relaxed: (item: type is scalar, at: ptr cell(item))
                     -> (value: item)

The value at holds, with no ordering beyond the cell's own.

public new: (item: type is scalar, value: item) -> (made: cell(item))

A cell holding value, for a local or a field; module storage writes zeroed instead. Making it is not atomic: make it before another context can reach it.

public sole_add: (item: type is scalar, at: ptr mut cell(item), value: item)
                 -> (previous: item)

Add value to at, wrapping, as the cell's one writer, and return what it held: a relaxed load and a release store, so a reader that acquires the sum sees what the writer did before. It is correct only while one context alone writes the cell, an interrupt handler or a thread, since a second writer's store between the two is lost; then it needs no read-modify-write, and every target has it, Cortex-M0 included. Where several contexts write, use add.

public store: (item: type is scalar, at: ptr mut cell(item), value: item)
              -> none

Store value into at, with release ordering: whatever this context wrote before it is visible to a load that acquires this value.

store_relaxed

function core/atomic/atomic.ldn:235 ↑ index
public store_relaxed: (item: type is scalar, at: ptr mut cell(item),
                       value: item) -> none

Store value into at, with no ordering beyond the cell's own.

Example program

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

The program, 35 lines
compiler/tests/fixtures/runtime/atomic-tick-firmware/main.ldn35 lines
--  Prototype 5's firmware/tick: time from an interrupt, with one writer,
--  the handler, and any number of readers.  The writer needs no
--  read-modify-write, which Cortex-M0 has not: sole_add is a relaxed load
--  and a release store, and a reader's load acquires.  A runtime fixture
--  places no vector, so the program is the writer here and counts where
--  it would wait; environments/cortex-m/probes/fibre-capacity.ldn runs the
--  same writer as SysTick's handler on Cortex-M0.
import core/atomic

mut ticks: atomic.cell(u32) = zeroed

count_tick: () -> none =
    _ = atomic.sole_add(addr ticks, 1)
end count_tick

now_ticks: () -> (count: u32) =
    count = atomic.load(addr ticks)
end now_ticks

--  Wait for three ticks after the first reading, as a fibre waiting on the
--  clock does, with a bound so that a count that never moves fails.
public main: () -> (code: i32) =
    code = 1
    first: u32 = now_ticks()
    mut seen: u32 = first
    mut waits: u32 = 0
    while seen - first < 3 and waits < 1000 do
        count_tick()
        seen = now_ticks()
        inc waits
    end while
    if seen - first == 3 and waits == 3 then
        code = 42
    end if
end main