Software Transactional Memory for MoonBit: TVars, atomic and composable transactions, retry/or_else blocking, TMVar and TQueue, plus an async runner.
///|
test "atomic transfer between accounts" {
let alice = @transaction.TVar::new(100)
let bob = @transaction.TVar::new(20)
@transaction.atomically(txn => {
let funds = txn.read(alice)
txn.check(funds >= 30) // block (retry) while funds are short
txn.write(alice, funds - 30)
txn.modify(bob, balance => balance + 30)
})
inspect(alice.peek(), content="70")
inspect(bob.peek(), content="50")
}| Package | Purpose | Depends on |
|---|---|---|
| azhzx/transaction | Core: TVar, Txn, atomically, TMVar, TQueue | nothing beyond core |
| azhzx/transaction/runner | atomically_async: transactions whose body may await | moonbitlang/async |
| Package | wasm | wasm-gc | js | native |
|---|---|---|---|---|
| core | ✓ | ✓ | ✓ | ✓ |
| runner | ✓ | — | ✓ | ✓ |
moon add azhzx/transaction///|
import {
"azhzx/transaction",
"azhzx/transaction/runner",
} // only if you need async transactions///|
test "read, write, and peek" {
let counter = @transaction.TVar::new(0)
let doubled = @transaction.atomically(txn => {
txn.modify(counter, x => x + 1)
txn.read(counter) * 2
})
inspect(doubled, content="2")
inspect(counter.peek(), content="1")
}///|
test "retry blocks the transaction, not the program" {
let balance : @transaction.TVar[Int] = @transaction.TVar::new(0)
try
@transaction.atomically(txn => {
txn.check(txn.read(balance) >= 10)
txn.modify(balance, x => x - 10)
})
catch {
err => assert_true(@transaction.is_retry(err))
} noraise {
_ => fail("expected the transaction to block")
}
inspect(balance.peek(), content="0")
}///|
test "or_else tries the first branch, then the second" {
let urgent : @transaction.TMVar[String] = @transaction.TMVar::new()
let regular = @transaction.TMVar::full("regular-mail")
let message = @transaction.atomically(txn => {
txn.or_else(t => urgent.take(t), t => regular.take(t))
})
inspect(message, content="regular-mail")
}///|
async test "consumer parks until a producer commits" {
let jobs : @transaction.TQueue[Int] = @transaction.TQueue::new()
let received = Ref(0)
@aio.with_task_group(group => {
group.spawn_bg(() => {
received.val = @runner.atomically_async(txn => jobs.pop(txn))
})
@runner.atomically_async(txn => jobs.push(txn, 42))
})
inspect(received.val, content="42")
}///|
test "queue hands work to a consumer in FIFO order" {
let jobs = @transaction.TQueue::new()
@transaction.atomically(txn => {
jobs.push(txn, "first")
jobs.push(txn, "second")
})
inspect(@transaction.atomically(t => jobs.pop(t)), content="first")
inspect(
@transaction.atomically(t => jobs.try_pop(t)) is Some("second"),
content="true",
)
}moon test # wasm (default) — includes README and docstring tests
moon test --target native # native backend
moon bench --target native --release # benchmark suite (see doc/en.md)
moon run examples/showcase # runnable example (see below)
moon info && moon fmt # refresh .mbti interfaces and formatmoon run examples/showcase # wasm
moon run examples/showcase --target nativeafter transfer: alice=70, bob=50
consumer received: [10, 20, 30]pub suberror Conflict {
Conflict(Int)
}pub suberror Retry {
Retry(Int)
}pub struct TMVar[T] {
// private fields
}test {
let box = @transaction.TMVar::full("gift")
let got = @transaction.atomically(txn => box.take(txn))
inspect(got, content="gift")
assert_true(box.peek() is None)
}test {
let box = @transaction.TMVar::full(1)
assert_true(box.peek() is Some(1))
}test {
let box : @transaction.TMVar[Int] = @transaction.TMVar::new()
let empty = @transaction.atomically(txn => box.is_empty(txn))
inspect(empty, content="true")
}test {
let box = @transaction.TMVar::full(9)
assert_true(box.peek() is Some(9))
}test {
let box = @transaction.TMVar::new()
let txn = @transaction.Txn::begin()
txn.run(_ => box.put(txn, "v"))
assert_true(box.peek() is Some("v"))
}test {
let box = @transaction.TMVar::full(9)
let value = @transaction.atomically(txn => box.read(txn))
assert_true(value is Some(9))
}test {
let box = @transaction.TMVar::full("x")
let value = @transaction.atomically(txn => box.take(txn))
inspect(value, content="x")
}test {
let box = @transaction.TMVar::full("kept")
let txn = @transaction.Txn::begin()
inspect(txn.run(_ => box.try_put(txn, "dropped")), content="false")
assert_true(box.peek() is Some("kept"))
}test {
let box = @transaction.TMVar::full(1)
let txn = @transaction.Txn::begin()
assert_true(txn.run(_ => box.try_take(txn)) is Some(1))
assert_true(txn.run(_ => box.try_take(txn)) is None)
}pub struct TQueue[T] {
// private fields
}test {
let jobs = @transaction.TQueue::new()
let txn = @transaction.Txn::begin()
txn.run(_ => {
jobs.push(txn, "a")
jobs.push(txn, "b")
})
inspect(txn.run(t => jobs.pop(t)), content="a")
}test {
let queue : @transaction.TQueue[Int] = @transaction.TQueue::new()
let txn = @transaction.Txn::begin()
inspect(txn.run(t => queue.is_empty(t)), content="true")
}test {
let queue = @transaction.TQueue::new()
let txn = @transaction.Txn::begin()
txn.run(t => queue.push(t, "head"))
assert_true(txn.run(t => queue.peek(t)) is Some("head"))
}test {
let queue = @transaction.TQueue::new()
let txn = @transaction.Txn::begin()
txn.run(t => queue.push(t, 42))
inspect(txn.run(t => queue.pop(t)), content="42")
}test {
let queue = @transaction.TQueue::new()
let txn = @transaction.Txn::begin()
txn.run(t => queue.push(t, 1))
inspect(@transaction.atomically(t => queue.length(t)), content="1")
}test {
let queue : @transaction.TQueue[Int] = @transaction.TQueue::new()
let txn = @transaction.Txn::begin()
assert_true(txn.run(t => queue.try_pop(t)) is None)
}pub struct TVar[T] {
// private fields
}test {
let account = @transaction.TVar::new(100)
@transaction.atomically(txn => {
let balance = txn.read(account)
txn.check(balance >= 10)
txn.write(account, balance - 10)
})
inspect(account.peek(), content="90")
}test {
let cell = @transaction.TVar::new([1, 2, 3])
inspect(cell.peek().length(), content="3")
}test {
let cell = @transaction.TVar::new(5)
inspect(cell.peek(), content="5")
}pub struct Txn {
// private fields
}test {
let cell = @transaction.TVar::new(1)
let txn = @transaction.Txn::begin()
txn.write(cell, 2)
txn.run(_ => ())
inspect(cell.peek(), content="2")
}test {
let cell = @transaction.TVar::new(3)
let value = @transaction.atomically(txn => {
txn.check(txn.read(cell) > 0)
txn.read(cell)
})
inspect(value, content="3")
}test {
let cell = @transaction.TVar::new(1)
let txn = @transaction.Txn::begin()
txn.write(cell, 2)
txn.commit()
inspect(cell.peek(), content="2")
}test {
let counter = @transaction.TVar::new(7)
@transaction.atomically(txn => txn.modify(counter, x => x + 1))
inspect(counter.peek(), content="8")
}test {
let inbox = @transaction.TMVar::new()
let draft = @transaction.TMVar::full("draft")
let picked = @transaction.atomically(txn => {
txn.or_else(t => inbox.take(t), t => draft.take(t))
})
inspect(picked, content="draft")
}test {
let cell = @transaction.TVar::new("hello")
let value = @transaction.atomically(txn => txn.read(cell))
inspect(value, content="hello")
}test {
let balance = @transaction.TVar::new(0)
try
@transaction.atomically(txn => {
let funds = txn.read(balance)
txn.check(funds >= 100)
txn.write(balance, funds - 100)
})
catch {
err => assert_true(@transaction.is_retry(err))
} noraise {
_ => fail("expected the transaction to retry")
}
inspect(balance.peek(), content="0")
}test {
let cell = @transaction.TVar::new(1)
let txn = @transaction.Txn::begin()
txn.write(cell, 99)
txn.rollback()
inspect(cell.peek(), content="1")
txn.run(t => t.write(cell, 2))
inspect(cell.peek(), content="2")
}test {
let cell = @transaction.TVar::new(1)
@transaction.atomically(txn => txn.write(cell, 41 + 1))
inspect(cell.peek(), content="42")
}test {
let balance = @transaction.TVar::new(100)
@transaction.atomically(txn => {
let current = txn.read(balance)
txn.write(balance, current - 30)
})
inspect(balance.peek(), content="70")
}fn commit_version() -> Intfn is_conflict(err : Error) -> Boolfn is_retry(err : Error) -> Booltest {
let cell : @transaction.TVar[Int] = @transaction.TVar::new(0)
try
@transaction.atomically(txn => {
txn.check(txn.read(cell) > 0)
txn.read(cell)
})
catch {
err => assert_true(@transaction.is_retry(err))
} noraise {
_ => fail("expected the transaction to block")
}
}fn on_commit(listener : () -> Unit) -> UnitInstall
Download zipSoftware Transactional Memory for MoonBit: TVars, atomic and composable transactions, retry/or_else blocking, TMVar and TQueue, plus an async runner.