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,3 @@
bump: minor
type: feat
Jobs, Promises & opt-in Sync — a layered concurrency library (#14). The safe default is `Job.*` / `Promise.*`: a `Job` is a future — start background work with `Job.run(kind, arg)` (a cooperative compute that advances a little each `Job.pump(budget)` and finishes after enough frames, so heavy work spreads out instead of hitching one), or drive one by hand with `Job.defer` + `Job.fulfill` / `Job.fail` / `Job.cancel`; poll it with `done`/`ok`/`failed`/`cancelled`, read `result`/`error`, and count outstanding work with `Job.pending`. `Promise.all`/`Promise.race` combine handle lists into a group job resolved on the main thread, and `Promise.count_done`/`Promise.all_done` give a loading bar its numerator and its ready check. The advanced, opt-in `Sync.*` tier — `mutex`/`lock`/`unlock`/`try_lock`, an atomic counter (`atomic`/`get`/`set`/`add`/`cas`) and a bounded int `channel` (`send`/`recv`/`can_recv`/`len`), plus `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 at a point you choose and a Job never touches the ECS world directly, so lockstep networking and replays stay bit-exact — the same jobs and the 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 first-class functions, so a Job carries a compute kind + int argument (or a hand-driven `defer`) rather than a closure, and Promise progress is polled rather than chained through a `then`. Written in Ludic and spliced on demand (like Regex/Dict/Numeric), so a program that never mentions `Job.*`/`Promise.*`/`Sync.*` compiles byte-identically and the C-free bootstrap fixpoint is untouched.

View file

@ -0,0 +1,7 @@
---
id: job
title: Job
order: 37
---
Background work that stays out of the frame. A <code>Job</code> is a future — a handle to a result that lands later. Kick one off with <code>Job.run</code> (a background compute that advances a little each <code>Job.pump</code> and finishes after enough frames, so heavy work never hitches) or <code>Job.defer</code> (a future you resolve yourself with <code>Job.fulfill</code> / <code>Job.fail</code>). Poll it with <code>done</code> / <code>ok</code> / <code>failed</code> / <code>cancelled</code>, read <code>result</code> / <code>error</code>, and always collect on the main thread — a Job must never touch the ECS world directly. The scheduler is deterministic and cooperative, so the same jobs and the same budget reproduce byte-for-byte, every run and every target. Arguments are positional. Spliced in only when a program mentions <code>Job.*</code>.

View file

@ -0,0 +1,19 @@
---
id: job-cancel
name: Job.cancel
category: job
kind: namespace-method
tokens: Job.cancel
sig: Job.cancel(handle) -> void
tip: Cancel a job before it finishes.
order: 4
ns: Job
member: cancel
---
Cancel a job before it finishes.
```ludic
let job = Job.defer()
Job.cancel(job)
```

View file

@ -0,0 +1,20 @@
---
id: job-cancelled
name: Job.cancelled
category: job
kind: namespace-method
tokens: Job.cancelled
sig: Job.cancelled(handle) -> bool
tip: Was the job cancelled?
order: 9
ns: Job
member: cancelled
---
Was the job cancelled?
```ludic
let job = Job.defer()
Job.cancel(job)
let stopped = Job.cancelled(job)
```

View file

@ -0,0 +1,19 @@
---
id: job-defer
name: Job.defer
category: job
kind: namespace-method
tokens: Job.defer
sig: Job.defer() -> Job
tip: A future you resolve yourself later.
order: 0
ns: Job
member: defer
---
A future you resolve yourself later.
```ludic
let job = Job.defer()
Job.fulfill(job, 42)
```

View file

@ -0,0 +1,20 @@
---
id: job-done
name: Job.done
category: job
kind: namespace-method
tokens: Job.done
sig: Job.done(handle) -> bool
tip: Has the job resolved (any outcome)?
order: 6
ns: Job
member: done
---
Has the job resolved (any outcome)?
```ludic
let job = Job.defer()
Job.fulfill(job, 1)
let finished = Job.done(job)
```

View file

@ -0,0 +1,20 @@
---
id: job-error
name: Job.error
category: job
kind: namespace-method
tokens: Job.error
sig: Job.error(handle) -> int
tip: The error code of a failed job.
order: 11
ns: Job
member: error
---
The error code of a failed job.
```ludic
let job = Job.defer()
Job.fail(job, 500)
let code = Job.error(job)
```

View file

@ -0,0 +1,19 @@
---
id: job-fail
name: Job.fail
category: job
kind: namespace-method
tokens: Job.fail
sig: Job.fail(handle, error) -> void
tip: Resolve a pending job as failed.
order: 3
ns: Job
member: fail
---
Resolve a pending job as failed.
```ludic
let job = Job.defer()
Job.fail(job, 404)
```

View file

@ -0,0 +1,20 @@
---
id: job-failed
name: Job.failed
category: job
kind: namespace-method
tokens: Job.failed
sig: Job.failed(handle) -> bool
tip: Did the job fail?
order: 8
ns: Job
member: failed
---
Did the job fail?
```ludic
let job = Job.defer()
Job.fail(job, 9)
let bad = Job.failed(job)
```

View file

@ -0,0 +1,20 @@
---
id: job-free
name: Job.free
category: job
kind: namespace-method
tokens: Job.free
sig: Job.free(handle) -> void
tip: Release a job slot back to the pool.
order: 13
ns: Job
member: free
---
Release a job slot back to the pool.
```ludic
let job = Job.defer()
Job.fulfill(job, 1)
Job.free(job)
```

View file

@ -0,0 +1,19 @@
---
id: job-fulfill
name: Job.fulfill
category: job
kind: namespace-method
tokens: Job.fulfill
sig: Job.fulfill(handle, value) -> void
tip: Resolve a pending job with a value.
order: 2
ns: Job
member: fulfill
---
Resolve a pending job with a value.
```ludic
let job = Job.defer()
Job.fulfill(job, 7)
```

View file

@ -0,0 +1,20 @@
---
id: job-ok
name: Job.ok
category: job
kind: namespace-method
tokens: Job.ok
sig: Job.ok(handle) -> bool
tip: Did the job succeed?
order: 7
ns: Job
member: ok
---
Did the job succeed?
```ludic
let job = Job.defer()
Job.fulfill(job, 1)
let good = Job.ok(job)
```

View file

@ -0,0 +1,18 @@
---
id: job-pending
name: Job.pending
category: job
kind: namespace-method
tokens: Job.pending
sig: Job.pending() -> int
tip: How many jobs are still unresolved.
order: 12
ns: Job
member: pending
---
How many jobs are still unresolved.
```ludic
let left = Job.pending() # a ready-made loading-screen counter
```

View file

@ -0,0 +1,19 @@
---
id: job-pump
name: Job.pump
category: job
kind: namespace-method
tokens: Job.pump
sig: Job.pump(budget) -> int
tip: Advance background jobs; collect results.
order: 5
ns: Job
member: pump
---
Advance background jobs; collect results.
```ludic
let job = Job.run(1, 1000)
Job.pump(64) # spend up to 64 steps this frame (0 = finish all)
```

View file

@ -0,0 +1,20 @@
---
id: job-result
name: Job.result
category: job
kind: namespace-method
tokens: Job.result
sig: Job.result(handle) -> int
tip: The success value of a done job.
order: 10
ns: Job
member: result
---
The success value of a done job.
```ludic
let job = Job.defer()
Job.fulfill(job, 42)
let value = Job.result(job)
```

View file

@ -0,0 +1,19 @@
---
id: job-run
name: Job.run
category: job
kind: namespace-method
tokens: Job.run
sig: Job.run(kind, arg) -> Job
tip: Start a background compute job.
order: 1
ns: Job
member: run
---
Start a background compute job.
```ludic
let job = Job.run(1, 100) # 1 = sum 1..arg, 2 = fib, 3 = count primes
Job.pump(0)
```

View file

@ -0,0 +1,7 @@
---
id: promise
title: Promise
order: 38
---
Combine several <code>Job</code> futures and resolve the group on the main thread. <code>Promise.all</code> succeeds once every member has, <code>Promise.race</code> once the first does; both return an ordinary job handle you poll like any other. For a loading screen, <code>Promise.count_done</code> over the same handles is the bar's numerator and <code>len</code> the denominator, and <code>Promise.all_done</code> is the ready check. Ludic has no closures, so progress is polled rather than chained through a <code>then</code> callback. Build the handle list with <code>new []int</code> + <code>push</code>. Spliced in only when a program mentions <code>Promise.*</code>.

View file

@ -0,0 +1,21 @@
---
id: promise-all
name: Promise.all
category: promise
kind: namespace-method
tokens: Promise.all
sig: Promise.all(handles) -> Job
tip: Succeeds when every member succeeds.
order: 0
ns: Promise
member: all
---
Succeeds when every member succeeds.
```ludic
let hs = new []int
push(hs, a)
push(hs, b)
let loaded = Promise.all(hs)
```

View file

@ -0,0 +1,21 @@
---
id: promise-all_done
name: Promise.all_done
category: promise
kind: namespace-method
tokens: Promise.all_done
sig: Promise.all_done(handles) -> bool
tip: Have all members resolved?
order: 3
ns: Promise
member: all_done
---
Have all members resolved?
```ludic
let hs = new []int
push(hs, a)
push(hs, b)
let ready = Promise.all_done(hs)
```

View file

@ -0,0 +1,21 @@
---
id: promise-count_done
name: Promise.count_done
category: promise
kind: namespace-method
tokens: Promise.count_done
sig: Promise.count_done(handles) -> int
tip: How many members have resolved.
order: 2
ns: Promise
member: count_done
---
How many members have resolved.
```ludic
let hs = new []int
push(hs, a)
push(hs, b)
let progress = Promise.count_done(hs)
```

View file

@ -0,0 +1,21 @@
---
id: promise-race
name: Promise.race
category: promise
kind: namespace-method
tokens: Promise.race
sig: Promise.race(handles) -> Job
tip: Succeeds when the first member does.
order: 1
ns: Promise
member: race
---
Succeeds when the first member does.
```ludic
let hs = new []int
push(hs, a)
push(hs, b)
let first = Promise.race(hs)
```

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)
```

113
examples/library/jobs.ludic Normal file
View file

@ -0,0 +1,113 @@
# jobs.ludic — Job.* / Promise.* (safe, deterministic) + Sync.* (advanced,
# opt-in). Each assertion that holds prints its number, so a full run prints:
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
# One cooperative scheduler backs it all (see runtime/native/jobs.ludic): a
# background Job runs a little each Job.pump and its result is collected on the
# main thread — no hitches, no locks in game code, and byte-identical every run.
program Concurrency {
entry {
# --- Job: background compute, collected on the main thread ---
# kinds: 1 = sum 1..n, 2 = fib(n), 3 = count primes <= n
let sum = Job.run(1, 100) # 1 + 2 + ... + 100 = 5050
Job.pump(0) # budget 0 = run every job to completion
if Job.ok(sum) and Job.result(sum) == 5050 { print(1) }
let fib = Job.run(2, 10) # fib(10) = 55
Job.pump(0)
if Job.result(fib) == 55 { print(2) }
let primes = Job.run(3, 20) # primes <= 20: 2 3 5 7 11 13 17 19 -> 8
Job.pump(0)
if Job.result(primes) == 8 { print(3) }
# cooperative: a job finishes only after enough total budget has been spent,
# so heavy work is spread across frames instead of hitching one.
let slow = Job.run(1, 10) # needs 10 steps; 1+..+10 = 55
Job.pump(3) # 3 of 10
if not Job.done(slow) { print(4) }
Job.pump(3) # 6 of 10
if not Job.done(slow) { print(5) }
Job.pump(100) # finishes this frame
if Job.done(slow) and Job.result(slow) == 55 { print(6) }
# hand-driven future: defer now, fulfill later (no closures needed)
let f = Job.defer()
if not Job.done(f) { print(7) }
Job.fulfill(f, 42)
if Job.ok(f) and Job.result(f) == 42 { print(8) }
# error path: a job can fail with a code
let e = Job.defer()
Job.fail(e, 9)
if Job.failed(e) and Job.error(e) == 9 { print(9) }
# cancel path
let c = Job.defer()
Job.cancel(c)
if Job.cancelled(c) and Job.done(c) and not Job.ok(c) { print(10) }
# --- Promise: combine futures, resolve on the main thread ---
let a1 = Job.defer()
let a2 = Job.defer()
let a3 = Job.defer()
let all = new []int
push(all, a1)
push(all, a2)
push(all, a3)
let grp = Promise.all(all)
if not Job.done(grp) { print(11) }
if Promise.count_done(all) == 0 { print(12) } # loading bar: 0 / 3
Job.fulfill(a1, 1)
Job.fulfill(a2, 2)
if Promise.count_done(all) == 2 { print(13) } # 2 / 3
if not Promise.all_done(all) { print(14) }
Job.fulfill(a3, 3)
if Promise.all_done(all) { print(15) } # 3 / 3
if Job.ok(grp) and Job.result(grp) == 3 { print(16) }
# race: the first member to succeed wins; its handle is the result
let b1 = Job.defer()
let b2 = Job.defer()
let two = new []int
push(two, b1)
push(two, b2)
let winner = Promise.race(two)
Job.fulfill(b2, 77)
if Job.ok(winner) and Job.result(winner) == b2 { print(17) }
# Promise.all fails once the set settles with a failure
let d1 = Job.defer()
let d2 = Job.defer()
let dd = new []int
push(dd, d1)
push(dd, d2)
let dgrp = Promise.all(dd)
Job.fulfill(d1, 1)
Job.fail(d2, 5)
if Job.failed(dgrp) { print(18) }
# --- Sync: the advanced, opt-in tier (cooperative + deterministic today) ---
let m = Sync.mutex()
Sync.lock(m)
if not Sync.try_lock(m) { print(19) } # already held
Sync.unlock(m)
if Sync.try_lock(m) { print(20) } # now free
let at = Sync.atomic()
if Sync.add(at, 5) == 5 { print(21) }
if Sync.add(at, 3) == 8 { print(22) }
if Sync.cas(at, 8, 100) { print(23) } # 8 -> 100
if Sync.get(at) == 100 { print(24) }
if not Sync.cas(at, 8, 0) { print(25) } # stale expect, no swap
let ch = Sync.channel()
if Sync.send(ch, 10) { print(26) }
Sync.send(ch, 20)
Sync.send(ch, 30)
if Sync.len(ch) == 3 { print(27) }
if Sync.recv(ch) == 10 { print(28) } # FIFO order
if Sync.recv(ch) == 20 { print(29) }
if Sync.can_recv(ch) { print(30) } # one left (30)
if Sync.cpu_count() >= 1 { print(31) }
}
}

537
runtime/native/jobs.ludic Normal file
View file

@ -0,0 +1,537 @@
# ============================================================================
# jobs.ludic — Jobs, Promises & opt-in Sync, in Ludic (#14).
#
# Concurrency that keeps the *simple* thing simple. Ludic's simulation is
# single-threaded and deterministic on purpose — the ECS schedule, lockstep
# networking and replays all depend on it — so this library is layered:
#
# 1. Job.* / Promise.* (the safe default): kick off work, keep the frame
# moving, and collect the result *on the main thread* at a point you choose
# (Job.pump). No shared mutable state, no locks in game code.
# 2. Sync.* (advanced, opt-in — "here be dragons"): mutex, atomic
# counters and channels for engine-level systems that pass messages around.
#
# The whole thing is a deterministic *cooperative* scheduler: a background Job
# runs a little each Job.pump(budget) and finishes after enough frames, so a
# heavy computation (terrain, pathfinding pre-bake, a checksum) is spread out
# instead of hitching one frame. Same jobs + same total budget over time ->
# byte-identical results and completion order, every run and every target
# (native and wasm alike). That determinism is why it is cooperative rather than
# a preemptive OS-thread pool: a real-thread backend can slot in behind this same
# API later without changing a line of game code, and the safe tier keeps its
# hard rule either way — collect results on the main thread; never let a Job
# reach into the ECS world itself.
#
# Ludic has no first-class functions, so a Job carries a small compute *kind* +
# an integer argument (or you drive a hand-made future with Job.defer +
# Job.fulfill) rather than a closure, and Promise progress is polled
# (Promise.count_done) rather than chained through a callback. ludicc splices
# this file into any program that mentions Job.* / Promise.* / Sync.*; it is
# self-contained (only compiler intrinsics), so a plain tool works like a game.
# The namespaces (emit_call.ludic) alias each method to a function below.
# ============================================================================
const JOB_SLOTS: int = 128 # concurrent jobs (handles are 1-based slot ids)
const JOB_MAXMEM: int = 64 # members a single Promise group can hold
# job state (the terminal states are DONE / FAILED / CANCELLED)
const J_FREE: int = 0 # slot not allocated
const J_PENDING: int = 1 # allocated, not yet resolved
const J_DONE: int = 2 # resolved successfully -> result
const J_FAILED: int = 3 # resolved with an error -> error
const J_CANCELLED: int = 4 # cancelled before it finished
# job kind (what a PENDING slot is)
const JK_DEFER: int = 0 # a hand-driven future (Job.fulfill / Job.fail)
const JK_SUM: int = 1 # compute: 1 + 2 + ... + arg
const JK_FIB: int = 2 # compute: the arg-th Fibonacci number
const JK_PRIMES: int = 3 # compute: how many primes are <= arg
const JK_ALL: int = 4 # Promise group: succeeds when every member does
const JK_RACE: int = 5 # Promise group: succeeds when the first member does
var jb_ready: bool = false
var jb_state: words = null # J_*
var jb_kind: words = null # JK_*
var jb_result: words = null # the success value
var jb_error: words = null # the failure code
var jb_arg: words = null # compute input n
var jb_i: words = null # compute progress counter
var jb_acc: words = null # compute accumulator
var jb_acc2: words = null # compute second accumulator (Fibonacci)
var jb_nmem: words = null # group member count
var jb_mem: words = null # flat [JOB_SLOTS * JOB_MAXMEM] of member handles
function jb_init() -> void {
if jb_ready { return }
jb_state = words(JOB_SLOTS); fill(jb_state, 0, JOB_SLOTS * 4)
jb_kind = words(JOB_SLOTS); fill(jb_kind, 0, JOB_SLOTS * 4)
jb_result = words(JOB_SLOTS); fill(jb_result, 0, JOB_SLOTS * 4)
jb_error = words(JOB_SLOTS); fill(jb_error, 0, JOB_SLOTS * 4)
jb_arg = words(JOB_SLOTS); fill(jb_arg, 0, JOB_SLOTS * 4)
jb_i = words(JOB_SLOTS); fill(jb_i, 0, JOB_SLOTS * 4)
jb_acc = words(JOB_SLOTS); fill(jb_acc, 0, JOB_SLOTS * 4)
jb_acc2 = words(JOB_SLOTS); fill(jb_acc2, 0, JOB_SLOTS * 4)
jb_nmem = words(JOB_SLOTS); fill(jb_nmem, 0, JOB_SLOTS * 4)
jb_mem = words(JOB_SLOTS * JOB_MAXMEM); fill(jb_mem, 0, JOB_SLOTS * JOB_MAXMEM * 4)
jb_ready = true
}
# claim a free slot as PENDING with the given kind; returns a 1-based handle, or
# 0 if the table is full.
function jb_alloc(kind: int) -> int {
jb_init()
var i = 0
while i < JOB_SLOTS {
if jb_state[i] == J_FREE {
jb_state[i] = J_PENDING
jb_kind[i] = kind
jb_result[i] = 0; jb_error[i] = 0
jb_arg[i] = 0; jb_i[i] = 0; jb_acc[i] = 0; jb_acc2[i] = 0
jb_nmem[i] = 0
return i + 1
}
i = i + 1
}
return 0
}
function jb_valid(h: int) -> bool {
jb_init()
if (h < 1) or (h > JOB_SLOTS) { return false }
return jb_state[h - 1] != J_FREE
}
# ---- the safe tier: futures ------------------------------------------------
# A hand-driven future: PENDING until you call Job.fulfill / Job.fail on it.
function job_defer() -> int { return jb_alloc(JK_DEFER) }
# Kick off a background compute job (kind = JK_SUM / JK_FIB / JK_PRIMES). It runs
# a little each Job.pump and resolves when it finishes. `arg` is its input.
function job_run(kind: int, arg: int) -> int {
let h = jb_alloc(kind)
if h == 0 { return 0 }
let s = h - 1
jb_arg[s] = arg
if kind == JK_FIB { jb_acc[s] = 0; jb_acc2[s] = 1 } # fib(0)=0, fib(1)=1
return h
}
# Resolve a pending job successfully with `value` (no-op once resolved).
function job_fulfill(h: int, value: int) -> void {
if not jb_valid(h) { return }
let s = h - 1
if jb_state[s] != J_PENDING { return }
jb_state[s] = J_DONE
jb_result[s] = value
}
# Resolve a pending job as failed with error code `err` (no-op once resolved).
function job_fail(h: int, err: int) -> void {
if not jb_valid(h) { return }
let s = h - 1
if jb_state[s] != J_PENDING { return }
jb_state[s] = J_FAILED
jb_error[s] = err
}
# Cancel a pending job (no-op if it already resolved).
function job_cancel(h: int) -> void {
if not jb_valid(h) { return }
let s = h - 1
if jb_state[s] == J_PENDING { jb_state[s] = J_CANCELLED }
}
# Recompute a group job (JK_ALL / JK_RACE) from its members. A no-op unless the
# slot is a still-PENDING group. This is what "resolve on the main thread" means:
# a Promise settles only when you look at it (done/ok/...) or pump.
function jb_refresh_group(s: int) -> void {
if jb_state[s] != J_PENDING { return }
let k = jb_kind[s]
if (k != JK_ALL) and (k != JK_RACE) { return }
let n = jb_nmem[s]
let base = s * JOB_MAXMEM
var i = 0
var settled = 0 # members in a terminal state
var ok = 0 # members that succeeded
var first_ok = 0 # winning handle for RACE
while i < n {
let mh = jb_mem[base + i]
if jb_valid(mh) {
let ms = mh - 1
let mst = jb_state[ms]
if mst != J_PENDING {
settled = settled + 1
if mst == J_DONE {
ok = ok + 1
if first_ok == 0 { first_ok = mh }
}
}
} else {
settled = settled + 1 # a freed/invalid member counts as settled-failed
}
i = i + 1
}
if k == JK_ALL {
if ok == n { jb_state[s] = J_DONE; jb_result[s] = n }
else { if settled == n { jb_state[s] = J_FAILED; jb_error[s] = n - ok } }
} else {
if first_ok != 0 { jb_state[s] = J_DONE; jb_result[s] = first_ok }
else { if settled == n { jb_state[s] = J_FAILED; jb_error[s] = n } }
}
}
# resolved in any terminal state?
function job_done(h: int) -> bool {
if not jb_valid(h) { return false }
jb_refresh_group(h - 1)
return jb_state[h - 1] != J_PENDING
}
function job_ok(h: int) -> bool {
if not jb_valid(h) { return false }
jb_refresh_group(h - 1)
return jb_state[h - 1] == J_DONE
}
function job_failed(h: int) -> bool {
if not jb_valid(h) { return false }
jb_refresh_group(h - 1)
return jb_state[h - 1] == J_FAILED
}
function job_cancelled(h: int) -> bool {
if not jb_valid(h) { return false }
return jb_state[h - 1] == J_CANCELLED
}
# the success value (0 unless the job is done-ok)
function job_result(h: int) -> int {
if not jb_valid(h) { return 0 }
jb_refresh_group(h - 1)
if jb_state[h - 1] != J_DONE { return 0 }
return jb_result[h - 1]
}
# the failure code (0 unless the job failed)
function job_error(h: int) -> int {
if not jb_valid(h) { return 0 }
jb_refresh_group(h - 1)
if jb_state[h - 1] != J_FAILED { return 0 }
return jb_error[h - 1]
}
# how many jobs are still pending (a ready-made loading-screen denominator).
function job_pending() -> int {
jb_init()
var n = 0
var i = 0
while i < JOB_SLOTS {
if jb_state[i] == J_PENDING { n = n + 1 }
i = i + 1
}
return n
}
# release a slot back to the pool.
function job_free(h: int) -> void {
if not jb_valid(h) { return }
jb_state[h - 1] = J_FREE
}
# advance one compute job by a single step; returns 1 if it just finished.
function jb_step(s: int) -> int {
let k = jb_kind[s]
let n = jb_arg[s]
var i = jb_i[s]
if k == JK_SUM {
jb_acc[s] = jb_acc[s] + (i + 1)
i = i + 1
jb_i[s] = i
if i >= n { jb_state[s] = J_DONE; jb_result[s] = jb_acc[s]; return 1 }
return 0
}
if k == JK_FIB {
if i >= n { jb_state[s] = J_DONE; jb_result[s] = jb_acc[s]; return 1 }
let t = jb_acc[s] + jb_acc2[s]
jb_acc[s] = jb_acc2[s]
jb_acc2[s] = t
i = i + 1
jb_i[s] = i
if i >= n { jb_state[s] = J_DONE; jb_result[s] = jb_acc[s]; return 1 }
return 0
}
if k == JK_PRIMES {
if jb_is_prime(i) { jb_acc[s] = jb_acc[s] + 1 }
i = i + 1
jb_i[s] = i
if i > n { jb_state[s] = J_DONE; jb_result[s] = jb_acc[s]; return 1 }
return 0
}
return 0
}
function jb_is_prime(v: int) -> bool {
if v < 2 { return false }
var d = 2
while d * d <= v {
if v - (v / d) * d == 0 { return false }
d = d + 1
}
return true
}
# Advance every pending compute job, spending up to `budget` steps in total, and
# resolve any Promise groups. Call it once per frame (or wherever you want the
# results to land). Returns how many jobs finished during this call. `budget` <=
# 0 means "run every compute job to completion right now".
function job_pump(budget: int) -> int {
jb_init()
var completed = 0
var spent = 0
var s = 0
while s < JOB_SLOTS {
let k = jb_kind[s]
let compute = (k == JK_SUM) or (k == JK_FIB) or (k == JK_PRIMES)
while (jb_state[s] == J_PENDING) and compute {
if (budget > 0) and (spent >= budget) { s = JOB_SLOTS + 1; break }
let fin = jb_step(s)
spent = spent + 1
if fin == 1 { completed = completed + 1 }
}
s = s + 1
}
# settle groups after the compute jobs advanced this frame.
s = 0
while s < JOB_SLOTS {
if jb_state[s] == J_PENDING {
let k = jb_kind[s]
if (k == JK_ALL) or (k == JK_RACE) {
jb_refresh_group(s)
if jb_state[s] != J_PENDING { completed = completed + 1 }
}
}
s = s + 1
}
return completed
}
# ---- Promise combinators (over a []int of job handles) ---------------------
# store up to JOB_MAXMEM handles as the members of group slot `s`.
function jb_set_members(s: int, handles: []int) -> void {
var n = len(handles)
if n > JOB_MAXMEM { n = JOB_MAXMEM }
let base = s * JOB_MAXMEM
var i = 0
while i < n { jb_mem[base + i] = handles[i]; i = i + 1 }
jb_nmem[s] = n
}
# A promise that succeeds once every member has succeeded, and fails as soon as
# the whole set has settled with at least one non-success. Returns a job handle.
function prom_all(handles: []int) -> int {
let h = jb_alloc(JK_ALL)
if h == 0 { return 0 }
jb_set_members(h - 1, handles)
jb_refresh_group(h - 1)
return h
}
# A promise that succeeds as soon as the first member succeeds (its handle is the
# result), and fails only if every member settles without success.
function prom_race(handles: []int) -> int {
let h = jb_alloc(JK_RACE)
if h == 0 { return 0 }
jb_set_members(h - 1, handles)
jb_refresh_group(h - 1)
return h
}
# how many of `handles` have resolved (any terminal state) — a loading bar's
# numerator; pair with len(handles) for the denominator.
function prom_count_done(handles: []int) -> int {
var n = 0
var i = 0
while i < len(handles) {
if job_done(handles[i]) { n = n + 1 }
i = i + 1
}
return n
}
function prom_all_done(handles: []int) -> bool {
var i = 0
while i < len(handles) {
if not job_done(handles[i]) { return false }
i = i + 1
}
return true
}
# ============================================================================
# Sync.* — the advanced, opt-in tier. HERE BE DRAGONS.
#
# These are the raw building blocks — a lock, an atomic counter, a channel — for
# engine-level systems that hand data between producers and consumers. On today's
# single-threaded, deterministic runtime they are cooperative: correct, ordered
# and replayable, and impossible to deadlock (there is one thread). They exist so
# a message-passing system reads the same in game code now as it will when a
# preemptive OS-thread backend lands behind this same API. Beginners never need
# to touch this — reach for Job.* / Promise.* instead.
# ============================================================================
const SYNC_MUTEX: int = 32
const SYNC_ATOMIC: int = 64
const SYNC_CHAN: int = 32
const CHAN_CAP: int = 64 # capacity of each channel's ring buffer
var sy_ready: bool = false
var mx_used: words = null
var mx_held: words = null
var at_used: words = null
var at_val: words = null
var ch_used: words = null
var ch_head: words = null
var ch_count: words = null
var ch_buf: words = null # flat [SYNC_CHAN * CHAN_CAP]
function sy_init() -> void {
if sy_ready { return }
mx_used = words(SYNC_MUTEX); fill(mx_used, 0, SYNC_MUTEX * 4)
mx_held = words(SYNC_MUTEX); fill(mx_held, 0, SYNC_MUTEX * 4)
at_used = words(SYNC_ATOMIC); fill(at_used, 0, SYNC_ATOMIC * 4)
at_val = words(SYNC_ATOMIC); fill(at_val, 0, SYNC_ATOMIC * 4)
ch_used = words(SYNC_CHAN); fill(ch_used, 0, SYNC_CHAN * 4)
ch_head = words(SYNC_CHAN); fill(ch_head, 0, SYNC_CHAN * 4)
ch_count = words(SYNC_CHAN); fill(ch_count, 0, SYNC_CHAN * 4)
ch_buf = words(SYNC_CHAN * CHAN_CAP); fill(ch_buf, 0, SYNC_CHAN * CHAN_CAP * 4)
sy_ready = true
}
# ---- mutex (a cooperative lock) --------------------------------------------
function sync_mutex() -> int {
sy_init()
var i = 0
while i < SYNC_MUTEX {
if mx_used[i] == 0 { mx_used[i] = 1; mx_held[i] = 0; return i + 1 }
i = i + 1
}
return 0
}
function sync_lock(m: int) -> void {
sy_init()
if (m < 1) or (m > SYNC_MUTEX) { return }
mx_held[m - 1] = 1
}
function sync_unlock(m: int) -> void {
sy_init()
if (m < 1) or (m > SYNC_MUTEX) { return }
mx_held[m - 1] = 0
}
# take the lock only if it is free; returns whether it was taken.
function sync_try_lock(m: int) -> bool {
sy_init()
if (m < 1) or (m > SYNC_MUTEX) { return false }
if mx_held[m - 1] != 0 { return false }
mx_held[m - 1] = 1
return true
}
# ---- atomic counter --------------------------------------------------------
function sync_atomic() -> int {
sy_init()
var i = 0
while i < SYNC_ATOMIC {
if at_used[i] == 0 { at_used[i] = 1; at_val[i] = 0; return i + 1 }
i = i + 1
}
return 0
}
function sync_get(a: int) -> int {
sy_init()
if (a < 1) or (a > SYNC_ATOMIC) { return 0 }
return at_val[a - 1]
}
function sync_set(a: int, v: int) -> void {
sy_init()
if (a < 1) or (a > SYNC_ATOMIC) { return }
at_val[a - 1] = v
}
# add `delta` and return the new value.
function sync_add(a: int, delta: int) -> int {
sy_init()
if (a < 1) or (a > SYNC_ATOMIC) { return 0 }
at_val[a - 1] = at_val[a - 1] + delta
return at_val[a - 1]
}
# compare-and-set: if the value equals `expect`, store `next` and return true.
function sync_cas(a: int, expect: int, next: int) -> bool {
sy_init()
if (a < 1) or (a > SYNC_ATOMIC) { return false }
if at_val[a - 1] != expect { return false }
at_val[a - 1] = next
return true
}
# ---- channel (a bounded int FIFO) ------------------------------------------
function sync_channel() -> int {
sy_init()
var i = 0
while i < SYNC_CHAN {
if ch_used[i] == 0 { ch_used[i] = 1; ch_head[i] = 0; ch_count[i] = 0; return i + 1 }
i = i + 1
}
return 0
}
# enqueue `v`; returns false if the channel is full.
function sync_send(c: int, v: int) -> bool {
sy_init()
if (c < 1) or (c > SYNC_CHAN) { return false }
let s = c - 1
if ch_count[s] >= CHAN_CAP { return false }
let pos = ch_head[s] + ch_count[s]
var idx = pos
if idx >= CHAN_CAP { idx = idx - CHAN_CAP }
ch_buf[s * CHAN_CAP + idx] = v
ch_count[s] = ch_count[s] + 1
return true
}
# dequeue the oldest value; returns 0 on an empty channel (guard with can_recv).
function sync_recv(c: int) -> int {
sy_init()
if (c < 1) or (c > SYNC_CHAN) { return 0 }
let s = c - 1
if ch_count[s] == 0 { return 0 }
let v = ch_buf[s * CHAN_CAP + ch_head[s]]
var nh = ch_head[s] + 1
if nh >= CHAN_CAP { nh = 0 }
ch_head[s] = nh
ch_count[s] = ch_count[s] - 1
return v
}
function sync_can_recv(c: int) -> bool {
sy_init()
if (c < 1) or (c > SYNC_CHAN) { return false }
return ch_count[c - 1] > 0
}
function sync_len(c: int) -> int {
sy_init()
if (c < 1) or (c > SYNC_CHAN) { return 0 }
return ch_count[c - 1]
}
# worker lanes available to the scheduler. One today (the deterministic main
# thread); a future OS-thread backend would report the real core count here.
function sync_cpu_count() -> int { return 1 }

View file

@ -389,6 +389,49 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if (meth == "clear") { bare = "set_clear"; push(labels, "s") }
if (meth == "members") { bare = "set_members"; push(labels, "s") }
}
# Job.* / Promise.* / Sync.* -> the concurrency runtime (runtime/native/jobs.ludic,
# spliced on demand). Ordinary Ludic functions, so the generic call path keeps
# their return types (int / bool). The safe tier (Job/Promise) is a deterministic
# cooperative scheduler; Sync.* is the advanced, opt-in message-passing tier. #14.
if (ns == "Job") {
if (meth == "defer") { bare = "job_defer" }
if (meth == "run") { bare = "job_run"; push(labels, "kind"); push(labels, "arg") }
if (meth == "fulfill") { bare = "job_fulfill"; push(labels, "handle"); push(labels, "value") }
if (meth == "fail") { bare = "job_fail"; push(labels, "handle"); push(labels, "error") }
if (meth == "cancel") { bare = "job_cancel"; push(labels, "handle") }
if (meth == "pump") { bare = "job_pump"; push(labels, "budget") }
if (meth == "done") { bare = "job_done"; push(labels, "handle") }
if (meth == "ok") { bare = "job_ok"; push(labels, "handle") }
if (meth == "failed") { bare = "job_failed"; push(labels, "handle") }
if (meth == "cancelled") { bare = "job_cancelled"; push(labels, "handle") }
if (meth == "result") { bare = "job_result"; push(labels, "handle") }
if (meth == "error") { bare = "job_error"; push(labels, "handle") }
if (meth == "pending") { bare = "job_pending" }
if (meth == "free") { bare = "job_free"; push(labels, "handle") }
}
if (ns == "Promise") {
if (meth == "all") { bare = "prom_all"; push(labels, "handles") }
if (meth == "race") { bare = "prom_race"; push(labels, "handles") }
if (meth == "count_done") { bare = "prom_count_done"; push(labels, "handles") }
if (meth == "all_done") { bare = "prom_all_done"; push(labels, "handles") }
}
if (ns == "Sync") {
if (meth == "mutex") { bare = "sync_mutex" }
if (meth == "lock") { bare = "sync_lock"; push(labels, "mutex") }
if (meth == "unlock") { bare = "sync_unlock"; push(labels, "mutex") }
if (meth == "try_lock") { bare = "sync_try_lock"; push(labels, "mutex") }
if (meth == "atomic") { bare = "sync_atomic" }
if (meth == "get") { bare = "sync_get"; push(labels, "atomic") }
if (meth == "set") { bare = "sync_set"; push(labels, "atomic"); push(labels, "value") }
if (meth == "add") { bare = "sync_add"; push(labels, "atomic"); push(labels, "delta") }
if (meth == "cas") { bare = "sync_cas"; push(labels, "atomic"); push(labels, "expect"); push(labels, "next") }
if (meth == "channel") { bare = "sync_channel" }
if (meth == "send") { bare = "sync_send"; push(labels, "channel"); push(labels, "value") }
if (meth == "recv") { bare = "sync_recv"; push(labels, "channel") }
if (meth == "can_recv") { bare = "sync_can_recv"; push(labels, "channel") }
if (meth == "len") { bare = "sync_len"; push(labels, "channel") }
if (meth == "cpu_count") { bare = "sync_cpu_count" }
}
# Huge.* / Angle.* / Percent.* -> the numeric runtime (runtime/native/numeric.ludic,
# spliced on demand). Ordinary Ludic functions, so the generic call path keeps
# their return types (Huge / fixed / int / bool).

View file

@ -178,6 +178,7 @@ function p_postfix() -> Node {
if e.a.kind == E_ID and (e.a.s == "BigInt" or e.a.s == "Decimal") { g_uses_bignum = true } # splice the bignum runtime on demand
if e.a.kind == E_ID and (e.a.s == "Dict" or e.a.s == "Set") { g_uses_dict = true } # splice the hash-table runtime on demand
if e.a.kind == E_ID and (e.a.s == "Huge" or e.a.s == "Angle" or e.a.s == "Percent") { g_uses_numeric = true } # splice the huge/angle/percent runtime on demand
if e.a.kind == E_ID and (e.a.s == "Job" or e.a.s == "Promise" or e.a.s == "Sync") { g_uses_jobs = true } # splice the jobs/promise/sync runtime on demand
if e.a.kind == E_ID and e.a.s == "Query" { g_uses_query = true } # splice the ECS spatial-query runtime on demand
if e.a.kind == E_ID and e.a.s == "Reflect" { g_uses_reflect = true } # force-emit the reflection ABI (Reflect.* reads the world schema)
if e.a.kind == E_ID and e.a.s == "Light" { g_uses_light = true } # splice the 2D light-accumulation pass on demand
@ -427,6 +428,7 @@ var g_uses_regex: bool = false # a program mentioned Regex.* -> splice the re
var g_uses_bignum: bool = false # a program mentioned BigInt.*/Decimal.* -> splice the bignum runtime
var g_uses_dict: bool = false # a program mentioned Dict.*/Set.* -> splice the hash-table runtime
var g_uses_numeric: bool = false # a program mentioned Huge.*/Angle.*/Percent.* -> splice the numeric runtime
var g_uses_jobs: bool = false # a program mentioned Job.*/Promise.*/Sync.* -> splice the concurrency runtime
var g_uses_query: bool = false # a program mentioned Query.* -> splice the query runtime + reflection ABI
var g_uses_reflect: bool = false # a program mentioned Reflect.* -> force-emit the reflection ABI
var g_uses_light: bool = false # a program mentioned Light.* -> splice the 2D light pass
@ -626,6 +628,14 @@ function maybe_splice_runtime() -> void {
do_import("runtime/native/numeric.ludic")
cur_dir = saved
}
# any program that uses Job.*/Promise.*/Sync.* gets the concurrency runtime
# spliced in (it is self-contained — only compiler intrinsics — so a plain tool
# works too). A deterministic cooperative scheduler; see runtime/native/jobs.ludic.
if g_uses_jobs {
cur_dir = ""
do_import("runtime/native/jobs.ludic")
cur_dir = saved
}
# any program that uses Query.* gets the ECS spatial-query helpers spliced in;
# they read entity state through the reflection ABI (emit_decl force-emits it
# for a Query program even when it declares no events).
@ -743,6 +753,7 @@ function parse_program() -> void {
g_uses_bignum = false
g_uses_dict = false
g_uses_numeric = false
g_uses_jobs = false
g_uses_query = false
g_uses_reflect = false
g_uses_esys = false

File diff suppressed because it is too large Load diff

View file

@ -437,6 +437,45 @@
"set-clear",
"set-members"
],
"job": [
"job-defer",
"job-run",
"job-fulfill",
"job-fail",
"job-cancel",
"job-pump",
"job-done",
"job-ok",
"job-failed",
"job-cancelled",
"job-result",
"job-error",
"job-pending",
"job-free"
],
"promise": [
"promise-all",
"promise-race",
"promise-count_done",
"promise-all_done"
],
"sync": [
"sync-mutex",
"sync-lock",
"sync-unlock",
"sync-try_lock",
"sync-atomic",
"sync-get",
"sync-set",
"sync-add",
"sync-cas",
"sync-channel",
"sync-send",
"sync-recv",
"sync-can_recv",
"sync-len",
"sync-cpu_count"
],
"huge": [
"huge-from",
"huge-add",

View file

@ -196,6 +196,7 @@ function cmd_test() -> int {
feat_case("library/bignum", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16", "bignum.ludic (BigInt arbitrary-precision + Decimal exact base-10 money)")
feat_case("library/containers", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "containers.ludic (Dict string-keyed hash map + Set string set)")
feat_case("library/numeric", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20", "numeric.ludic (Huge idle big-numbers + Angle wrapping radians + Percent clamped [0,1])")
feat_case("library/jobs", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31", "jobs.ludic (Job background compute/defer/fulfill/cancel + Promise all/race/progress + Sync mutex/atomic/channel; issue #14)")
feat_case("library/optionresult", "", "1 2 3 4 5 6 7 8 9 10 11 12", "optionresult.ludic (option some/none + result ok/err/try safety types)")
feat_case("library/regex", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "regex.ludic (Regex match/find/groups/classes/quantifiers/replace + linear-time safety)")
feat_case("library/grid", "", "1 2 3 4 5 6 7 8 9 10 11 12 13", "grid.ludic (Grid line/flood/line_of_sight + A* pathfinding over the tilemap)")