docs: rewrite the README for someone meeting the language
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m51s
commit-lint / conventional-commits (push) Successful in 1s

The README opened with three restatements of "no C", then a limitations table,
then a repo-layout map, and closed with Chrono Rift's keybindings — P2/Mage
`I`/`K` select, `J` confirm — which is a game manual, not a language README. It
never showed the language itself.

It now leads with what Ludic is, a compiling code sample, how to build, what the
language offers, and an honest status. The layout table is contributor material
and CONTRIBUTING already covers that ground.

Two things were not merely stylistic:

- Nine links pointed at wiki pages that no longer exist.
- "Language at a glance" advertised `system` with `reads`/`writes`, plus
  `requires`/`ensures`. None of those are keywords — the grammar has `handler`,
  and `System.*` is a namespace. That bullet described a vocabulary retired
  several releases ago.

The sample is verified to compile, every internal link resolves, and every
command named is one `x help` actually offers.

Also makes commit-lint survive a force-push: it linted `event.before..sha`
without checking that `before` still resolves, so rewriting or gc'ing that
commit failed the job with "Invalid revision range" on a push whose messages
were all valid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-05 14:25:07 +03:00
parent 80da7e6275
commit c9ee303b69
4 changed files with 128 additions and 147 deletions

View file

@ -35,10 +35,15 @@ jobs:
# - pull_request: base branch .. this commit # - pull_request: base branch .. this commit
# - push: the pushed range (event.before .. this commit) # - push: the pushed range (event.before .. this commit)
# - new branch / unknown: just the tip commit # - new branch / unknown: just the tip commit
# `event.before` is only usable if it still resolves: a force-push
# rewrites (and a gc can remove) the commit it names, which made this
# job fail with "Invalid revision range" on an otherwise clean push.
# Fall back to the tip commit in that case.
if [ -n "${BASE:-}" ]; then if [ -n "${BASE:-}" ]; then
git fetch --quiet origin "${BASE}" 2>/dev/null || true git fetch --quiet origin "${BASE}" 2>/dev/null || true
RANGE="origin/${BASE}..${GITHUB_SHA}" RANGE="origin/${BASE}..${GITHUB_SHA}"
elif [ -n "${BEFORE:-}" ] && ! printf '%s' "$BEFORE" | grep -qE '^0+$'; then elif [ -n "${BEFORE:-}" ] && ! printf '%s' "$BEFORE" | grep -qE '^0+$' \
&& git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then
RANGE="${BEFORE}..${GITHUB_SHA}" RANGE="${BEFORE}..${GITHUB_SHA}"
else else
RANGE="${GITHUB_SHA}~1..${GITHUB_SHA}" RANGE="${GITHUB_SHA}~1..${GITHUB_SHA}"

252
README.md
View file

@ -1,189 +1,149 @@
# Ludic # Ludic
Ludic is an **ahead-of-time compiled** language for 2D games with an A compiled language for 2D games. The entity-component system is part of the
entity-component core, a deterministic fixed-point runtime, and its graphics syntax, the runtime is deterministic fixed-point, and `ludicc` lowers Ludic
stack built into the language. `ludicc` lowers Ludic straight to LLVM IR and straight to LLVM IR — **no C is generated, compiled or linked in a build.**
emits a native binary — and **`ludicc` is itself written in Ludic**, compiles
its own source to a byte-exact fixpoint, and rebuilds from a checked-in IR seed
with **no C compiler in the loop**.
``` The compiler is written in Ludic. It compiles its own source to a byte-exact
.ludic ──► ludicc ──► LLVM IR ──► object ──► native binary fixpoint and rebuilds from a checked-in IR seed with clang alone; CI asserts
(in Ludic) that on every push.
- **Documentation:** <https://workshopsoft.pages.workshopsoft.io/ludic/>
- **API reference:** <https://workshopsoft.pages.workshopsoft.io/ludic/api.html>
- **Issues:** <https://git.workshopsoft.io/workshopsoft/ludic/issues>
```ludic
program Hello {
property Position { column: int = 0, row: int = 0 }
property Velocity { delta_x: int = 0, delta_y: int = 0 }
handler SpawnEnemies phase Start {
spawn Enemy { Position { column: 3, row: 4 }, Velocity { delta_x: 1, delta_y: 0 } }
spawn Enemy { Position { column: 10, row: 2 }, Velocity { delta_x: 0, delta_y: 1 } }
}
# a handler declares the entities it touches; the body runs
# once per match, with each property bound by name.
@Queries(these: [Position, Velocity])
handler AdvancePositions phase FixedUpdate {
Position.column += Velocity.delta_x
Position.row += Velocity.delta_y
}
}
``` ```
**No C is generated, compiled or linked in a build.** No interpreter, no ## Getting started
transpiler, no C runtime: the framebuffer, sprites, PNG/DEFLATE decoding,
TrueType text, the retained UI, the registers and the RNG are all written in
Ludic (`runtime/native/*.ludic`); only the window seam — the `win_*` functions
for the window, keys, mouse, cursor, gamepad and touch — is hand-written LLVM IR
against the platform ABI (`runtime/native/cocoa.ll`),
the same floor Rust and Swift stand on.
## Backends `bin/x` is the project's task runner: one native binary, written in Ludic and
compiled by Ludic, that replaces every build and test script. Bootstrapping it
| Backend | Status | is the only step Ludic cannot do for itself, since compiling Ludic needs a
|---|---| compiler — clang assembles the checked-in IR seed:
| **Native 2D** (macOS/Cocoa window; headless render for CI) | **Shipping** — the default `bin/x app` target. |
| **Web / wasm32** | **In progress.** The browser platform layer is in-tree and documented — `runtime/web/` (the `<canvas>` window `platform.js`, the libc-free `wasm.ll` floor) and a Node harness that diffs native vs. wasm frame-for-frame (`tools/ludic-web/run.mjs`). Emitting wasm was a capability of the retired C compiler and is **not yet re-wired on the self-hosted toolchain**; see [COMPILING.md](COMPILING.md). |
The same is true of `--target` cross-compilation and `--shared` libraries: both
are designed and documented, both lived in the old C compiler, and both are
pending re-implementation on the self-hosted native toolchain.
## Quick start
`bin/x` is the project's task runner — one native binary, written in Ludic and
compiled by Ludic, that replaces every build/test/bootstrap shell script.
Bootstrap it once from a clean checkout (the only step Ludic can't do for
itself, since compiling Ludic needs a compiler) with clang alone:
```bash ```bash
mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x
``` ```
Then build the whole toolchain and run a game: Then build the toolchain and run a game:
```bash ```bash
bin/x build # -> bin/{ludicc,ludic,x,ludic-fmt,ludic-lsp} bin/x build # -> bin/{ludicc,ludic,x,ludic-fmt,ludic-lsp}
bin/x app examples/games/snake.ludic # compile + open a native window bin/x app examples/games/snake.ludic # compile and open a native window
./build/snake ./build/snake
``` ```
Render a frame headlessly (what CI checks) — output lands in `build/`, never the Rendering is deterministic, so a frame can be produced without a window — this
repo root: is what CI diffs:
```bash ```bash
bin/x app examples/games/chronorift.ludic --headless bin/x app examples/games/snake.ludic --headless
mkdir -p build && printf 'ddddwww' | ./build/chronorift_headless # writes build/out.ppm printf 'ddddwww' | ./build/snake_headless # writes build/out.ppm
sips -s format png build/out.ppm --out frame.png
``` ```
Run the suites: `bin/x help` lists every command. [`examples/`](examples/README.md) is a tour
grouped by intent: games, rendering, ECS, events, networking, language features
and the standard library.
## The language
- **ECS in the syntax.** `property`, `model` and `handler` are keywords. Query
with `for (a, b) in query [A, B, {Tag}] where <expr> { … }`; `spawn` and
`despawn` recycle entity slots; `@`-annotations drive lifecycle hooks.
- **Deterministic by construction.** Q16.16 `fixed` arithmetic and a seeded RNG
give the same frame byte-for-byte on every run — the basis for replays,
lockstep netcode and golden-image tests.
- **Scenes and state machines.** `scene` / `layer` / `become` model
mutually-exclusive game states with enter and exit hooks; `match` / `machine`
/ `state` handle dispatch and per-entity FSMs.
- **Events and networking.** A cancellable event bus (`event` / `emit` / `@On`)
and networking primitives (`@Sync`, ownership, RPCs) over a built-in transport.
- **Batteries in the language.** Framebuffer primitives, PNG sprites, TrueType
text and a retained `ui` widget tree declared as data, plus a namespaced
standard library (`Math`, `Text`, `List`, `Random`, `Crypto`, `Tiled`, …).
- **Whole-world snapshots.** `save()` and `load()` serialize every entity,
property and program `var` in one call.
[LANGUAGE.md](LANGUAGE.md) is the full reference; the
[API reference](https://workshopsoft.pages.workshopsoft.io/ludic/api.html)
documents every symbol on its own page.
## Packages
Dependencies are identified by URL, resolved with minimal version selection, and
cached in a content-addressed store:
```bash ```bash
bin/x test # full regression: compiler builds from seed, every example, golden renders bin/x add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/
bin/x selfhost-test # correctness + the self-hosting / C-free bootstrap fixpoints bin/x get # install from package.ludic, write the lock
bin/x help # every command bin/x verify # check locked packages against the store
``` ```
Add a dependency (URL-as-identity, MVS resolution, content-addressed store): See [`docs/PACKAGES.md`](docs/PACKAGES.md) for the manifest and lockfile model.
```bash
bin/x add git.workshopsoft.io/user/pkg # resolve + fetch + link into ludic_modules/
bin/x get # install everything in package.ludic, write the lock
bin/x verify # check the locked packages against the store
```
See [`docs/PACKAGES.md`](docs/PACKAGES.md) for the manifest, lockfile and package model.
## Layout
| Path | What it is |
|------|-----------|
| [`selfhost/*.ludic`](selfhost/) | **the compiler, written in Ludic** — lexer, parser, and the LLVM-IR backend (ECS storage, queries, spawn, `match`/`machine`, UI, scenes, save/load, fixed-point). Built from `selfhost/ludicc.seed.ll` with clang alone. |
| [`selfhost/golden/renders.sha256`](selfhost/golden/renders.sha256) | text baseline of render-output hashes (replaces binary `.ppm` fixtures); regenerate with `bin/x golden`. |
| [`tools/x/*.ludic`](tools/x/) | **the task runner, written in Ludic** — one binary (`bin/x`) that builds, tests, bootstraps and reseeds the project, replacing every shell script. |
| [`runtime/native/`](runtime/native/) | the runtime **in Ludic** for the native path: `core` (framebuffer, input, RNG), `image`/`inflate` (PNG + DEFLATE, no zlib), `truetype` (glyph rasterizer), `ui` (retained widget tree); plus `cocoa.ll`, the macOS window seam in LLVM IR. |
| [`runtime/web/`](runtime/web/) | the browser platform layer: `platform.js` (the `<canvas>` window), `wasm.ll` (the libc-free floor), `index.html`. |
| [`examples/`](examples/README.md) | the example tour, grouped by intent — `games/`, `rendering/`, `ecs/`, `events/`, `networking/`, `lang/`, `library/`. See [examples/README.md](examples/README.md). |
| [`tools/ludic-tools/`](tools/ludic-tools/) | the editor toolchain **in Ludic**: `ludic-fmt` (formatter) and `ludic-lsp` (language server) — one lexer, one vocabulary shared by both. |
| [`tools/editors/`](tools/editors/README.md) | plugins for VS Code and JetBrains, plus config for Neovim, Helix, Emacs, Sublime and Zed. |
| [`docs/`](docs/) | the per-symbol API reference, regenerated into the docs site. |
| [`COMPILING.md`](COMPILING.md) | the native pipeline: `ludicc → LLVM IR → exe`, the `rt_*` runtime protocol, and the (pending) wasm/cross-compile/shared-library paths. |
The design and roadmap material lives on the **[wiki](https://git.workshopsoft.io/workshopsoft/ludic/wiki)**:
the [Events](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Design/Events),
[Networking](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Design/Networking),
[Scenes](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Design/Scenes),
[Lifecycle](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Design/Lifecycle),
[Mobile](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Design/Mobile) and
[Syntax-redesign](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Design/Syntax-Redesign)
design records, the [Bootstrap deep-dive](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Bootstrap),
and the [Luanti roadmap](https://git.workshopsoft.io/workshopsoft/ludic/wiki/Roadmap/Luanti).
The root keeps only this README plus the two user-facing references,
[`LANGUAGE.md`](LANGUAGE.md) and [`COMPILING.md`](COMPILING.md).
## Language at a glance
- `program` / `property` (typed fields + defaults) / `model` (named entity kinds)
/ `system` (`phase`, `@annotations`, `reads`/`writes`).
- ECS queries `for (a, b) in query [A, B, {Tag}] where <expr> { … }`,
`spawn`/`despawn` with slot reuse, `@`-driven lifecycle hooks.
- An **event bus** (`event` / `emit` / `@On`, cancellable, `@Public` promotion)
and **networking** primitives (`@Sync`, ownership, RPCs) over a built-in
loopback transport — all deterministic, all pure Ludic.
- `scene` / `layer` / `become`, `match` / `machine` + `state`.
- Types `int`, `fixed` (Q16.16), `bool`, `entity`, `str`, `byte`, typed buffers;
a growing namespaced **standard library** (`Math`, `Vector`, `Time`/`Date`/
`Duration`/`Clock`, `Random`, `Hash`, `Crypto`, sorting, …).
- Deterministic seeded RNG and `save()`/`load()` snapshot of the whole World.
- Built-in 2D: framebuffer primitives, PNG sprites, TrueType text, 9-slice, and
a retained `ui` widget tree declared as data.
See [LANGUAGE.md](LANGUAGE.md) for the full reference, and
[examples/README.md](examples/README.md) for runnable demos of each feature.
## Editor support ## Editor support
```bash ```bash
bin/x tools # -> bin/ludic-fmt, bin/ludic-lsp bin/x tools # -> bin/ludic-fmt, bin/ludic-lsp
``` ```
`ludic-lsp` speaks LSP 3.17 over stdio, so one binary serves every editor: `ludic-lsp` speaks LSP 3.17 over stdio, so one binary serves every editor:
context-aware completion, diagnostics from the compiler itself, completion, diagnostics from the compiler itself, go-to-definition and rename
go-to-definition and rename across `import`ed files, and comment-preserving across imports, and comment-preserving formatting. `ludic-fmt` is the same
formatting. `ludic-fmt` is the same formatter as a CLI, for pre-commit hooks and formatter as a CLI, for pre-commit hooks. Both understand ```` ```ludic ````
CI. Both also understand ```` ```ludic ```` fences in Markdown. Plugins and fences in Markdown. Plugins and drop-in config for VS Code, JetBrains, Neovim,
drop-in config are in [`tools/editors/`](tools/editors/README.md). Helix, Emacs, Sublime and Zed are in [`tools/editors/`](tools/editors/README.md).
## Chrono Rift — the flagship game ## Status
[`examples/games/chronorift.ludic`](examples/games/chronorift.ludic) is a The native 2D backend ships: a Cocoa window on macOS, a headless renderer for
playable co-op JRPG — overworld, dungeon, random encounters, a turn-based co-op CI, and the whole runtime — framebuffer, PNG/DEFLATE decoding, TrueType
battle, a boss, an item shop and snapshot save/load — split across modules under rasterizer, retained UI, RNG — written in Ludic under
[`games/chronorift/`](examples/games/chronorift/). Its art is CC0 [`runtime/native/`](runtime/native/). Only the window seam (`win_*`: window,
[Kenney](https://kenney.nl) sprites, decoded from PNG at runtime by the keys, mouse, cursor, gamepad, touch) is hand-written LLVM IR against the
Ludic-written PNG/DEFLATE decoder — no zlib, no external dependency. platform ABI, the same floor Rust and Swift stand on.
- **Overworld:** `WASD` move, `K` save, `L` load. The **web/wasm32 backend is not currently available.** The browser platform
- **Battle (local co-op):** P1/Knight `W`/`S` select, `Space` confirm; layer is in-tree under [`runtime/web/`](runtime/web/), but emitting wasm was a
P2/Mage `I`/`K` select, `J` confirm. capability of the retired C compiler and has not been re-wired on the
self-hosted toolchain. `--target` cross-compilation and `--shared` libraries are
in the same position. See [COMPILING.md](COMPILING.md).
## Status & roadmap Releases follow SemVer and are cut from changesets by `x release`, then built
and published by CI from the tag; see [CHANGELOG.md](CHANGELOG.md).
The compiler self-hosts to a byte-exact fixpoint and rebuilds from its IR seed
with no C compiler; the ECS runtime, windowed + headless 2D rendering, the event
bus, the deterministic networking stack, scenes, and save/load are all in place
and covered by `bin/x test`. CI gates every push and PR on the build, the test
suites, and that C-free fixpoint. The toolchain is versioned with SemVer
(`ludicc --version`); releases and the `CHANGELOG.md` are cut from changesets by
`x release`. Upgrading from 0.2? See the
[0.2 → 0.3 migration guide](docs/MIGRATION-0.2-to-0.3.md).
Active work and proposals — the standard library, a fuller type system,
rendering/animation/lighting extras, input, filesystem/IO, testing, and
re-wiring the web/wasm and cross-compile backends — are tracked as issues, not
inlined here:
- **Issues & proposals:** <https://git.workshopsoft.io/workshopsoft/ludic/issues>
- **Docs site (API reference):** <https://workshopsoft.pages.workshopsoft.io/ludic/>
- **Wiki (design & roadmap):** <https://git.workshopsoft.io/workshopsoft/ludic/wiki>
## Contributing ## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development loop [CONTRIBUTING.md](CONTRIBUTING.md) covers the development loop, the commit and
(`bin/x reseed` → `bin/x bootstrap-cfree` → `bin/x test`), the code and commit code conventions, how the bootstrap fixpoint works, and what a self-hosted CI
conventions, and how the bootstrap fixpoint works. Issue and pull-request runner needs. Issue and pull-request templates are under
templates live under [`.forgejo/`](.forgejo/). [`.forgejo/`](.forgejo/).
## License ## License
The Ludic compiler and runtime source are licensed under the The compiler and runtime are licensed under the
[Apache License 2.0](LICENSE) (`SPDX-License-Identifier: Apache-2.0`) — a [Apache License 2.0](LICENSE) (`SPDX-License-Identifier: Apache-2.0`).
permissive license with an explicit patent grant.
The bundled [Kenney](https://kenney.nl) art under `assets/kenney/` is The bundled [Kenney](https://kenney.nl) art under `assets/kenney/` is
third-party and released under **CC0 1.0** (public domain); each pack keeps its third-party and released under **CC0 1.0**; each pack keeps its own
own `License.txt`. Code and assets are licensed separately: Apache-2.0 covers `License.txt`. Code and assets are licensed separately — Apache-2.0 covers the
the source, not the art. source, not the art.

View file

@ -0,0 +1,7 @@
bump: patch
type: ci
The commit-lint workflow survives a force-push. It linted
`${{ github.event.before }}..${{ github.sha }}` without checking that `before`
still resolves, so rewriting or garbage-collecting that commit failed the job
with `fatal: Invalid revision range` on a push whose messages were all valid. It
now falls back to linting the tip commit when `before` is gone.

View file

@ -0,0 +1,9 @@
bump: patch
type: docs
The README is rewritten around what a reader needs first: what the language is,
a code sample, how to build it, and an honest status. Removed the repo-layout
table and the Chrono Rift keybindings (a game manual in a language README), the
nine links to wiki pages that no longer exist, and a "language at a glance"
bullet describing a retired vocabulary — it advertised `system`, `reads`,
`writes`, `requires` and `ensures`, none of which are keywords; the declaration
keyword is `handler`.