tur/stm-sync
TMVar and TChan, built on tvar + check.
Both STM docs used to hand-sketch these from tvar/* and check. This is that sketch, made real and testable. See docs/archive/stm-tmvar-tchan-stdlib.md.
Load it explicitly: (load "stdlib/stm-sync.tur").
## Why these are macros, not functions
atomically requires a **syntactic** stm block -- (atomically (f x)) where f returns a transaction is a hard error ("atomically requires an stm block as argument"). So an STM action cannot be a value returned from a function; it has to be spliced into the transaction lexically, which is what a macro does.
That constraint shapes the whole API. Each operation comes in two forms:
`tmvar-take-stm` -- the BODY only. Use inside your own `(stm ...)`,
composing several operations into ONE transaction
that retries as a unit.
`tmvar-take` -- the same body wrapped in its own
`(atomically (stm ...))`. Use for a standalone
operation.
Reach for the -stm form whenever two operations must succeed together; two separate tmvar-take calls are two transactions, and another thread can interleave between them.
## The empty marker
A TVar holds a ptr, and an empty slot is the null pointer -- the same representation the guides sketched. In a pointer-valued world "no value" IS null, so this is not an :int sentinel standing in for a real type. The consequence is real though and worth stating plainly: **a TMVar cannot hold 0 / null as a legitimate value**, because it is indistinguishable from empty. Store a boxed value if you need the full range.
Since: 2026-08-20
tmvar-new
(tmvar-new [v])
create a TMVar holding a value (full).
| v | the initial value; must not be 0 (see "The empty marker" above) |
A TVar handle serving as a full TMVar.
(let [mv (tmvar-new 42)] ...)
Since: 2026-08-20
tmvar-new-empty
(tmvar-new-empty)
create an empty TMVar.
A TVar handle serving as an empty TMVar.
(let [mv (tmvar-new-empty)] ...)
Since: 2026-08-20
tmvar-take-stm
(tmvar-take-stm [mv])
transaction BODY: block until full, then empty and return.
Splice into your own (stm ...) to compose with other operations in one transaction. The check makes the whole transaction retry while the slot is empty, so a waiter parks rather than spins.
(atomically (stm (tmvar-take-stm mv)))
Since: 2026-08-20
tmvar-take
(tmvar-take [mv])
block until the TMVar is full, then empty it and return it.
Standalone form: runs in its own transaction. When two operations must succeed together, use tmvar-take-stm inside one (stm ...) instead -- two tmvar-take calls are two transactions and another thread can interleave.
| mv | the TMVar |
The value that was in the slot, as a ptr.
(let [mv (tmvar-new 42)] (println (:: (tmvar-take mv) :int))) ; => 42
Since: 2026-08-20
tmvar-put-stm
(tmvar-put-stm [mv v])
transaction BODY: block until empty, then fill.
(atomically (stm (tmvar-put-stm mv 7)))
Since: 2026-08-20
tmvar-put
(tmvar-put [mv v])
block until the TMVar is empty, then fill it.
| mv | the TMVar | |
| v | the value to store; must not be 0 |
(let [mv (tmvar-new-empty)] (tmvar-put mv 7) (println (:: (tmvar-take mv) :int))) ; => 7
Since: 2026-08-20
tmvar-read-stm
(tmvar-read-stm [mv])
transaction BODY: block until full, return WITHOUT emptying.
The difference from tmvar-take-stm is the whole point: a reader that peeks leaves the value for the next taker.
(atomically (stm (tmvar-read-stm mv)))
Since: 2026-08-20
tmvar-read
(tmvar-read [mv])
block until full, return the value without emptying.
| mv | the TMVar |
The value in the slot, as a ptr. The slot stays full.
(let [mv (tmvar-new 42)] (println (:: (tmvar-read mv) :int)) ; => 42 (println (:: (tmvar-take mv) :int))) ; => 42 -- still there
Since: 2026-08-20
tmvar-full?
(tmvar-full? [mv])
is the TMVar currently full? Does not block.
| mv | the TMVar |
true when the slot holds a value.
(println (tmvar-full? (tmvar-new-empty))) ; => false
Since: 2026-08-20
tchan-cons-append
(tchan-cons-append [lst :int v :int] :int)
internal: append v to the end of cons list lst.
Returns a fresh list; the input is left alone, so a retried transaction that re-runs this cannot corrupt the queue it read.
tchan-cons-head
(tchan-cons-head [lst :int] :int)
internal: first element, or -1 for an empty list.
tchan-cons-tail
(tchan-cons-tail [lst :int] :int)
internal: the list without its first element.
tchan-cons-len
(tchan-cons-len [lst :int] :int)
internal: number of elements.
tchan-new
(tchan-new)
create an empty TChan.
A TVar handle serving as an empty channel.
(let [ch (tchan-new)] ...)
Since: 2026-08-20
tchan-write-stm
(tchan-write-stm [ch v])
transaction BODY: append a value. Never blocks.
(atomically (stm (tchan-write-stm ch 1)))
Since: 2026-08-20
tchan-write
(tchan-write [ch v])
append a value to the channel. Never blocks (unbounded).
| ch | the TChan | |
| v | the value to append |
(let [ch (tchan-new)] (tchan-write ch 1))
Since: 2026-08-20
tchan-read-stm
(tchan-read-stm [ch])
transaction BODY: block until non-empty, then pop.
The check retries the whole transaction while the channel is empty, so a reader parks instead of spinning. Splice into your own (stm ...) to pop and do something else atomically.
(atomically (stm (tchan-read-stm ch)))
Since: 2026-08-20
tchan-read
(tchan-read [ch])
block until the channel is non-empty, then pop the oldest.
FIFO: the value returned is the one written earliest.
| ch | the TChan |
The oldest value in the channel.
(let [ch (tchan-new)] (tchan-write ch 1) (tchan-write ch 2) (println (tchan-read ch))) ; => 1
Since: 2026-08-20
tchan-len
(tchan-len [ch])
how many values are queued. Does not block.
| ch | the TChan |
The number of queued values.
(println (tchan-len (tchan-new))) ; => 0
Since: 2026-08-20
tchan-empty?
(tchan-empty? [ch])
is the channel empty? Does not block.
| ch | the TChan |
true when nothing is queued.
(println (tchan-empty? (tchan-new))) ; => true
Since: 2026-08-20