Proposal: Jobs & async concurrency (Job/Promise safe by default; Sync.* threads/mutex opt-in) #14
Labels
No labels
area:ci
area:docs
area:input
area:net
area:rendering
area:repo
area:stdlib
area:tooling
area:types
cleanup
dx
priority:high
priority:low
priority:medium
proposal
status:in-progress
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: workshopsoft/ludic#14
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
A high-level concurrency library centered on jobs and promises for
background work — asset loading, procedural generation, pathfinding, saving —
without exposing non-experts to the footguns of raw threads and locks.
Design philosophy (important for this language)
Ludic's gameplay simulation should stay single-threaded and deterministic
(the ECS scheduler, lockstep networking, and replays all depend on it). So this
library is deliberately layered:
run work off the main thread, get the result back on the main thread next
frame. No shared mutable state, no locks in user code.
engine-level systems. Clearly documented as "here be dragons," gated behind an
explicit namespace so a beginner never trips over them.
Proposed API (illustrative)
Why it matters for game devs
frame.
Job.run+job.done().Considerations
jobs must not mutate the ECS world directly. Document this as a hard rule.
threads by default).
asyncsugar as a follow-up once the primitives land.Scope / acceptance
Job.run/done/result/cancel+ worker pool.Promise.all/race/thenwith main-thread resolution.Sync.*(mutex, channel) namespace with prominent warnings.Related: error handling, Filesystem (async load), networking proposals.
Shipped in
50ecb84(pushed tomain).What landed
A layered concurrency library, written in Ludic and spliced on demand (like
Regex/Dict/Numeric), so a program that never mentions it compiles byte-identically and the C-free bootstrap fixpoint is untouched.Safe default —
Job.*/Promise.*:Job.run(kind, arg)— a background compute (kind1sum,2fib,3count-primes) that advances a little eachJob.pump(budget)and finishes after enough frames, so heavy work spreads across frames instead of hitching one.Job.defer+Job.fulfill/Job.fail/Job.cancel— a hand-driven future.Job.done/ok/failed/cancelled,Job.result/error,Job.pending,Job.free.Promise.all/Promise.racereturn an ordinary job handle resolved on the main thread;Promise.count_done/Promise.all_donegive a loading bar its numerator and its ready check.Advanced, opt-in —
Sync.*("here be dragons"):mutex/lock/unlock/try_lock, an atomic counter (atomic/get/set/add/cas), a bounded intchannel(send/recv/can_recv/len), andcpu_count.How it maps to the proposal
The design philosophy — simulation stays single-threaded and deterministic; results collected on the main thread at a defined point; jobs never mutate the ECS world — is the backbone here. The scheduler is a deterministic cooperative one: the same jobs and the same budget reproduce byte-for-byte on every target (native and wasm alike), which is exactly what lockstep networking and replays need, and what the acceptance criterion "deterministic completion" rewards. A preemptive OS-thread backend can slot behind this same API later without touching game code.
Two deliberate deviations, forced by the language (and documented in the runtime + docs):
Job.run(fn() -> …)andPromise.then(fn)aren't expressible. Instead a Job carries a compute kind + int argument (or you driveJob.defer/Job.fulfill), and Promise progress is polled (count_done) rather than chained through a callback — which is the better fit for a poll-based engine loop anyway.Acceptance checklist
Job.run/done/result/cancel(+defer/fulfill/fail/pump/pending/…) + a cooperative worker.Promise.all/racewith main-thread resolution (+count_done/all_donefor progress;thenreplaced by polling — see note 1).Sync.*(mutex, atomic, channel) namespace with prominent "here be dragons" warnings.examples/library/jobs.ludic), 33 namespace-method pages underdocs/language/{job,promise,sync}/.examples/library/jobs.ludic, 31 self-asserting checks wired intox test(all 86 pass), plusx test-tools,x check-impl/check-vocabulary/check-docs, and the C-free bootstrap fixpoint (out.ll == seed.ll) all green.Files
runtime/native/jobs.ludic— the runtime.selfhost/backend/emit_call.ludic—Job.*/Promise.*/Sync.*dispatch.selfhost/frontend/parse.ludic— on-demand splice.examples/library/jobs.ludic,tools/x/test.ludic— example + test wiring.docs/language/{job,promise,sync}/,tools/docgen/inventory.json— docs.changes/jobs-concurrency.md— changeset.Follow-ups worth their own issues: a real preemptive OS-thread backend behind this API, and coroutine/
asyncsugar once first-class functions land (tracked with #1).