feat(stdlib): Jobs, Promises & opt-in Sync concurrency (#14)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 15s
ci / build-and-test (push) Successful in 1m28s
commit-lint / conventional-commits (push) Successful in 2s
docs / build-and-deploy (push) Successful in 23s

A layered concurrency library, safe by default. The recommended tier is
Job.* / Promise.*: a Job is a future — Job.run(kind, arg) starts a
cooperative background compute that advances each Job.pump(budget) and
finishes after enough frames (heavy work spreads out instead of hitching),
or Job.defer + Job.fulfill/fail/cancel drives one by hand. Poll with
done/ok/failed/cancelled, read result/error, count outstanding work with
Job.pending. Promise.all/race combine handle lists into a group job resolved
on the main thread; Promise.count_done/all_done power a loading bar.

The advanced, opt-in Sync.* tier (mutex/atomic/channel + cpu_count) is the
"here be dragons" surface for engine-level message passing.

The whole thing is a deterministic cooperative scheduler: results are
collected on the main thread and a Job never touches the ECS world, so
lockstep and replays stay bit-exact — same jobs + same budget reproduce
byte-for-byte on every target, and a preemptive OS-thread backend can slot
behind this same API later. Ludic has no closures, so a Job carries a
compute kind + int arg (or a hand-driven defer) rather than fn()->…, and
Promise progress is polled rather than chained through then.

Written in Ludic and spliced on demand (like Regex/Dict/Numeric): a program
that never mentions Job.*/Promise.*/Sync.* compiles byte-identically and the
C-free bootstrap fixpoint is untouched. New: runtime/native/jobs.ludic,
emit_ns_call dispatch, parse-time splice, examples/library/jobs.ludic (31
self-asserting checks), 33 docs pages + inventory, changeset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-01 03:32:31 +03:00
parent 872f458cb2
commit 50ecb8472f
44 changed files with 25637 additions and 22806 deletions

View file

@ -0,0 +1,7 @@
---
id: sync
title: Sync
order: 39
---
The advanced, opt-in tier — <strong>here be dragons</strong>. Raw building blocks for engine-level systems that pass data around: a <code>mutex</code> (cooperative lock), an <code>atomic</code> counter (<code>get</code> / <code>set</code> / <code>add</code> / <code>cas</code>) and a bounded <code>channel</code> (<code>send</code> / <code>recv</code> / <code>can_recv</code> / <code>len</code>). On today's single-threaded deterministic runtime these are cooperative — correct, ordered, replayable and impossible to deadlock — and exist so message-passing code reads the same now as it will when a preemptive OS-thread backend lands behind this same API. Beginners never need this; reach for <code>Job.*</code> / <code>Promise.*</code> instead. Spliced in only when a program mentions <code>Sync.*</code>.

View file

@ -0,0 +1,19 @@
---
id: sync-add
name: Sync.add
category: sync
kind: namespace-method
tokens: Sync.add
sig: Sync.add(atomic, delta) -> int
tip: Add to the counter; return the new value.
order: 7
ns: Sync
member: add
---
Add to the counter; return the new value.
```ludic
let a = Sync.atomic()
let total = Sync.add(a, 1)
```

View file

@ -0,0 +1,20 @@
---
id: sync-atomic
name: Sync.atomic
category: sync
kind: namespace-method
tokens: Sync.atomic
sig: Sync.atomic() -> int
tip: Create an atomic counter (starts at 0).
order: 4
ns: Sync
member: atomic
---
Create an atomic counter (starts at 0).
Returns an atomic-counter handle. Read it with <code>Sync.get</code>, write with <code>Sync.set</code>, accumulate with <code>Sync.add</code>, and swap conditionally with <code>Sync.cas</code>.
```ludic
let a = Sync.atomic()
```

View file

@ -0,0 +1,19 @@
---
id: sync-can_recv
name: Sync.can_recv
category: sync
kind: namespace-method
tokens: Sync.can_recv
sig: Sync.can_recv(channel) -> bool
tip: Is there a value waiting?
order: 12
ns: Sync
member: can_recv
---
Is there a value waiting?
```ludic
let ch = Sync.channel()
let has = Sync.can_recv(ch)
```

View file

@ -0,0 +1,19 @@
---
id: sync-cas
name: Sync.cas
category: sync
kind: namespace-method
tokens: Sync.cas
sig: Sync.cas(atomic, expect, next) -> bool
tip: Compare-and-set the counter.
order: 8
ns: Sync
member: cas
---
Compare-and-set the counter.
```ludic
let a = Sync.atomic()
let swapped = Sync.cas(a, 0, 1)
```

View file

@ -0,0 +1,20 @@
---
id: sync-channel
name: Sync.channel
category: sync
kind: namespace-method
tokens: Sync.channel
sig: Sync.channel() -> int
tip: Create a bounded int FIFO channel.
order: 9
ns: Sync
member: channel
---
Create a bounded int FIFO channel.
Returns a channel handle — a fixed-capacity queue of ints for handing values between a producer and a consumer. Push with <code>Sync.send</code>, pull the oldest with <code>Sync.recv</code>, and check with <code>Sync.can_recv</code> / <code>Sync.len</code>.
```ludic
let ch = Sync.channel()
```

View file

@ -0,0 +1,18 @@
---
id: sync-cpu_count
name: Sync.cpu_count
category: sync
kind: namespace-method
tokens: Sync.cpu_count
sig: Sync.cpu_count() -> int
tip: Worker lanes available to the scheduler.
order: 14
ns: Sync
member: cpu_count
---
Worker lanes available to the scheduler.
```ludic
let lanes = Sync.cpu_count()
```

View file

@ -0,0 +1,19 @@
---
id: sync-get
name: Sync.get
category: sync
kind: namespace-method
tokens: Sync.get
sig: Sync.get(atomic) -> int
tip: Read the counter.
order: 5
ns: Sync
member: get
---
Read the counter.
```ludic
let a = Sync.atomic()
let v = Sync.get(a)
```

View file

@ -0,0 +1,19 @@
---
id: sync-len
name: Sync.len
category: sync
kind: namespace-method
tokens: Sync.len
sig: Sync.len(channel) -> int
tip: How many values are queued.
order: 13
ns: Sync
member: len
---
How many values are queued.
```ludic
let ch = Sync.channel()
let n = Sync.len(ch)
```

View file

@ -0,0 +1,19 @@
---
id: sync-lock
name: Sync.lock
category: sync
kind: namespace-method
tokens: Sync.lock
sig: Sync.lock(mutex) -> void
tip: Take the lock.
order: 1
ns: Sync
member: lock
---
Take the lock.
```ludic
let m = Sync.mutex()
Sync.lock(m)
```

View file

@ -0,0 +1,20 @@
---
id: sync-mutex
name: Sync.mutex
category: sync
kind: namespace-method
tokens: Sync.mutex
sig: Sync.mutex() -> int
tip: Create a cooperative lock.
order: 0
ns: Sync
member: mutex
---
Create a cooperative lock.
Creates a mutex handle for guarding a critical section. On the deterministic single-threaded runtime it never blocks — pair <code>Sync.lock</code> / <code>Sync.unlock</code> around the section, or probe with <code>Sync.try_lock</code>.
```ludic
let m = Sync.mutex()
```

View file

@ -0,0 +1,20 @@
---
id: sync-recv
name: Sync.recv
category: sync
kind: namespace-method
tokens: Sync.recv
sig: Sync.recv(channel) -> int
tip: Dequeue the oldest value.
order: 11
ns: Sync
member: recv
---
Dequeue the oldest value.
```ludic
let ch = Sync.channel()
Sync.send(ch, 42)
let v = Sync.recv(ch)
```

View file

@ -0,0 +1,19 @@
---
id: sync-send
name: Sync.send
category: sync
kind: namespace-method
tokens: Sync.send
sig: Sync.send(channel, value) -> bool
tip: Enqueue a value (false if full).
order: 10
ns: Sync
member: send
---
Enqueue a value (false if full).
```ludic
let ch = Sync.channel()
let sent = Sync.send(ch, 42)
```

View file

@ -0,0 +1,19 @@
---
id: sync-set
name: Sync.set
category: sync
kind: namespace-method
tokens: Sync.set
sig: Sync.set(atomic, value) -> void
tip: Store a value in the counter.
order: 6
ns: Sync
member: set
---
Store a value in the counter.
```ludic
let a = Sync.atomic()
Sync.set(a, 10)
```

View file

@ -0,0 +1,19 @@
---
id: sync-try_lock
name: Sync.try_lock
category: sync
kind: namespace-method
tokens: Sync.try_lock
sig: Sync.try_lock(mutex) -> bool
tip: Take the lock only if it is free.
order: 3
ns: Sync
member: try_lock
---
Take the lock only if it is free.
```ludic
let m = Sync.mutex()
let got = Sync.try_lock(m)
```

View file

@ -0,0 +1,20 @@
---
id: sync-unlock
name: Sync.unlock
category: sync
kind: namespace-method
tokens: Sync.unlock
sig: Sync.unlock(mutex) -> void
tip: Release the lock.
order: 2
ns: Sync
member: unlock
---
Release the lock.
```ludic
let m = Sync.mutex()
Sync.lock(m)
Sync.unlock(m)
```