docs: correct stale references across LANGUAGE, README, COMPILING, CONTRIBUTING

- LANGUAGE.md: real diagnostic format, list literals, Font.load / Ui.*
  (no `reg`/`set_reg`, no `/Handler/Library` typo), `extern function`,
  existing example links, Input.key(); builtins table trimmed to what exists
- COMPILING.md: pipeline names selfhost/ (not compiler/*.c), `program`
  instead of game/module, all thirteen win_* entry points by group
- README.md: the window seam is not "five" functions
- CONTRIBUTING.md: docs are checked with `x docs-gen` / `x docs-check`
- docs/language: kw-ui and fn-ui_build use the namespaced API;
  @ClearColor documents constant expressions; examples/README lists
  operators.ludic

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-05 01:12:26 +03:00
parent bf36bc8a8f
commit 583783449a
6 changed files with 65 additions and 53 deletions

View file

@ -27,7 +27,7 @@
> >
> The binaries are multi-call (one native binary under two names): invoked as > The binaries are multi-call (one native binary under two names): invoked as
> `ludicc` it compiles, as `ludic` it compiles-and-runs. A `.ludic` file with > `ludicc` it compiles, as `ludic` it compiles-and-runs. A `.ludic` file with
> systems is a game and links windowed by default; `--headless` and `--windowed` > handlers is a game and links windowed by default; `--headless` and `--windowed`
> force the mode. The runtime (`runtime/native/cocoa.ll`) is found via > force the mode. The runtime (`runtime/native/cocoa.ll`) is found via
> `$LUDIC_HOME`, defaulting to the directory the binary sits in — keep them in > `$LUDIC_HOME`, defaulting to the directory the binary sits in — keep them in
> `bin/`, or set `LUDIC_HOME` and put them on `PATH`. `$LUDIC_CC` overrides the > `bin/`, or set `LUDIC_HOME` and put them on `PATH`. `$LUDIC_CC` overrides the
@ -42,10 +42,10 @@ and find your program rewritten in another language.
``` ```
app.ludic app.ludic
│ ludicc — lex, parse, check, lower (compiler/ludicc.c, │ ludicc — lex, parse, lower (selfhost/frontend/*.ludic,
▼ compiler/native.c) ▼ selfhost/backend/*.ludic)
app.ll LLVM IR: your systems, your properties, your runtime app.ll LLVM IR: your handlers, your properties, your runtime
│ IR assembler (compiler/driver.c) │ IR assembler (selfhost/main.ludic drives $LUDIC_CC)
▼ ▼
app.o Mach-O / ELF / COFF object code app.o Mach-O / ELF / COFF object code
│ system linker │ system linker
@ -90,15 +90,16 @@ windowed and `--headless` builds today.
> **Not yet on the self-hosted toolchain.** `--shared` and the `nm`/library > **Not yet on the self-hosted toolchain.** `--shared` and the `nm`/library
> workflow below describe the old C driver's behavior; the self-hosted `ludicc` > workflow below describe the old C driver's behavior; the self-hosted `ludicc`
> builds executables only for now. The `module`/`@export fn` semantics are > builds executables only for now. The `@export function` semantics are
> unchanged — only the packaging step is pending. > unchanged — only the packaging step is pending.
A source file opens with `game Name { … }` or `module Name { … }`. A source file opens with `program Name { … }`.
* A **game** gets an entry point and the phase-ordered frame loop * A program with **handlers** is a game: it gets the phase-ordered frame loop
(`Start`, then `Input → FixedUpdate → Update → LateUpdate → Render` each tick). (`Start`, then `Input → FixedUpdate → Update → LateUpdate → Render` each tick).
* A **module** gets neither. It is a library, and only its `@export fn`s become * A program with only an **`entry`** block is a tool: it runs `entry` and exits.
public symbols; everything else stays private to the library. * Either kind can be a library: only its `@export function`s become public
symbols; everything else stays private.
```ludic ```ludic
# doc-check: skip — illustrative: elided body # doc-check: skip — illustrative: elided body
@ -196,8 +197,11 @@ intrinsics compile to nothing there, so a headless binary never references a
symbol the window would have provided. symbol the window would have provided.
Other platforms build headless today. A Win32 or X11 port is another `.ll` file Other platforms build headless today. A Win32 or X11 port is another `.ll` file
with the same five entry points — `win_open`, `win_poll`, `win_present`, with the same entry points — the window (`win_open`, `win_poll`, `win_present`,
`win_running`, `win_close` — and no compiler change. `win_running`, `win_close`), keys (`win_held`, `win_held_bit`), the mouse and
cursor (`win_mouse`, `win_cursor_mode`, `win_cursor_confine`,
`win_cursor_maintain`), gamepad (`win_pad`) and touch (`win_touch`) — and no
compiler change.
## The web ## The web

View file

@ -61,7 +61,7 @@ The stdlib lives in the runtime (`runtime/`) and is surfaced as namespaces
and register its id in `tools/docgen/inventory.json`. Each documented and register its id in `tools/docgen/inventory.json`. Each documented
namespace gets exactly **one** directory (the docs check enforces this). namespace gets exactly **one** directory (the docs check enforces this).
3. Add or extend an example under `examples/` and a case in the test suite. 3. Add or extend an example under `examples/` and a case in the test suite.
4. Run `python3 tools/docgen/gen.py && python3 tools/docgen/check.py` — the 4. Run `bin/x docs-gen --out build/pages && bin/x docs-check build/pages` — the
check fails if any inventory symbol lacks a page or is still seed text. check fails if any inventory symbol lacks a page or is still seed text.
5. Add a **changeset** for the user-facing change: a small file under 5. Add a **changeset** for the user-facing change: a small file under
[`changes/`](changes/README.md) with a `bump:` level and a one-line summary. [`changes/`](changes/README.md) with a `bump:` level and a one-line summary.

View file

@ -27,7 +27,7 @@ program Name {
model ... # a named entity KIND (bundle of properties) model ... # a named entity KIND (bundle of properties)
const ... # compile-time constants const ... # compile-time constants
function ... # functions function ... # functions
extern function … # bind a C library symbol (FFI) extern function … # bind a C library symbol (FFI)
enum ... # a named set of integer values enum ... # a named set of integer values
namespace ... # a block of functions with export / internal visibility namespace ... # a block of functions with export / internal visibility
handler ... # behavior, grouped into phases handler ... # behavior, grouped into phases
@ -50,11 +50,12 @@ declarations, no `program` wrapper. Its
declarations are spliced into the importing program. Imports may appear inside declarations are spliced into the importing program. Imports may appear inside
the `program` block or before it, they may nest (a fragment may import fragments), the `program` block or before it, they may nest (a fragment may import fragments),
and each resolved path is **include-guarded**, so importing the same file twice and each resolved path is **include-guarded**, so importing the same file twice
(even via different chains) pulls it in once. Diagnostics report the true file: (even via different chains) pulls it in once. Diagnostics name the file the
line really lives in, in the `file:line: error: message` shape editors already
parse:
``` ```
error: line 1: unknown type 'nope' for field Pos.x chronorift/world.ludic:1: error: expected expression
chronorift/world.ludic:1 | property Pos { x: nope = 0 }
``` ```
## Models (entity kinds) ## Models (entity kinds)
@ -93,7 +94,7 @@ prefab Grunt: Foe { Stats { hp: 30, max_hp: 30 }, Weapon { def_id: WeaponId.Bite
prefab Boss: Foe { Stats { hp: 400, max_hp: 400 }, Sprite { scale: 4 } } prefab Boss: Foe { Stats { hp: 400, max_hp: 400 }, Sprite { scale: 4 } }
spawn Grunt { Position { x: 40, y: 60 } } # Foe's presets, Grunt's, then this spawn Grunt { Position { x: 40, y: 60 } } # Foe's presets, Grunt's, then this
let boss = spawn Boss { Position { x: 160, y: 40 } } # spawn is also an expression: the entity let boss = spawn Boss { Position { x: 160, y: 40 } } # spawn is also an expression: the entity
let e = Prefab.spawn(name: kind_name) # chosen at runtime by name (-1 if none) let e = Prefab.spawn(name: kind_name) # chosen at runtime by name (-1 if none)
``` ```
@ -107,7 +108,7 @@ The 5×7 bitmap `text` stays for zero-asset programs. For real typography, load
TrueType font and draw UTF-8: TrueType font and draw UTF-8:
```ludic ```ludic
let f = font_load("/Handler/Library/Fonts/Supplemental/Arial.ttf") let f = Font.load("/System/Library/Fonts/Supplemental/Arial.ttf")
text_ttf(f, 20, 20, "Héllo — Καλημέρα — Привет", 0xffffff, 28) # anti-aliased text_ttf(f, 20, 20, "Héllo — Καλημέρα — Привет", 0xffffff, 28) # anti-aliased
let w = text_w(f, "measure me", 28) # pixel width let w = text_w(f, "measure me", 28) # pixel width
``` ```
@ -129,11 +130,13 @@ panels with padding / gap / alignment / grow), drawing (9-slice skins, images,
TrueType text, focus highlight) and keyboard focus + activation. TrueType text, focus highlight) and keyboard focus + activation.
```ludic ```ludic
var title_font: int = 0
ui MainMenu { ui MainMenu {
panel id: Root w: 288 pad: 16 gap: 6 skin: "assets/ui/panel.png" inset: 10 align: center { panel id: Root w: 288 pad: 16 gap: 6 skin: "assets/ui/panel.png" inset: 10 align: center {
label text: "CHRONO RIFT" font: reg(R_FONT) size: 26 fg: 0xffe060 align: center label text: "CHRONO RIFT" font: title_font size: 26 fg: Color.Gold align: center
button id: NewGame text: "New Game" font: reg(R_FONT) size: 16 w: 236 button id: NewGame text: "New Game" font: title_font size: 16 w: 236
button id: Quit text: "Quit" font: reg(R_FONT) size: 16 w: 236 button id: Quit text: "Quit" font: title_font size: 16 w: 236
} }
} }
``` ```
@ -142,22 +145,22 @@ A widget inherits `font`, `size`, `fg` and `align` from the nearest ancestor tha
sets them, so a panel states a menu's look once and a label only says what differs. sets them, so a panel states a menu's look once and a label only says what differs.
Widget types: `panel` (container + optional skin/bg/border), `col` / `row` Widget types: `panel` (container + optional skin/bg/border), `col` / `row`
(pure stacks), `label`, `button` (focusable), `image`, `spacer`. Props are (pure stacks), `label`, `button` (focusable), `image`, `spacer`. Props are
evaluated at build time, so `font: reg(R_FONT)` reads a value the program set first. evaluated at build time, so `font: title_font` reads a value the program set first.
Each `id: Name` mints a `UI_Name` handle (the `ui` block name too), used from Each `id: Name` mints a `UI_Name` handle (the `ui` block name too), used from
handlers: handlers:
```ludic ```ludic
handler Boot phase Start { handler Boot phase Start {
set_reg(R_FONT, font_load("…Arial.ttf")) title_font = Font.load("…Arial.ttf")
ui_build() # construct the tree (loads skins/images) Ui.build() # construct the tree (loads skins/images)
ui_open(UI_MainMenu) # make it active, focus the first button Ui.open(UI_MainMenu) # make it active, focus the first button
} }
handler Nav phase Update { handler Nav phase Update {
ui_tick(key()) # w/s move focus, space/enter activate Ui.tick(Input.key()) # w/s move focus, space/enter activate
if ui_clicked(UI_Quit) { quit() } if Ui.clicked(UI_Quit) { quit() }
ui_set_int(UI_HpLabel, hp) # poke dynamic values by id Ui.set_text(UI_HpLabel, `HP {hp}`) # poke dynamic values by id
} }
handler Draw phase Render { clear(0x0e0e16); ui_render(); present() } handler Draw phase Render { Screen.clear(Color.Black); Ui.render(); Screen.show() }
``` ```
The frame loop ticks navigation on its own, and an activation fires the The frame loop ticks navigation on its own, and an activation fires the
@ -308,7 +311,7 @@ giving that entity — the query header lifts out of the body into an annotation
```ludic ```ludic
# doc-check: skip — illustrative handler # doc-check: skip — illustrative handler
@Queries(these: [Battle{hp <= 0}, Pos], on: Enemy) @Queries(these: [Battle { hp <= 0 }, Pos], on: Enemy)
handler CleanBattle phase LateUpdate { despawn self() } handler CleanBattle phase LateUpdate { despawn self() }
``` ```
@ -335,7 +338,7 @@ matched on their field values:
```ludic ```ludic
# doc-check: skip — illustrative @Queries constraint # doc-check: skip — illustrative @Queries constraint
@Queries(these: [Battle{hp <= 0}, Stats{level > 3}]) @Queries(these: [Battle { hp <= 0 }, Stats { level > 3 }])
``` ```
The same `where` works on an inline `for (…) in query […]`; in `@Queries` the The same `where` works on an inline `for (…) in query […]`; in `@Queries` the
@ -463,7 +466,7 @@ property Transform { x: int = 0, scale: int = 1 }
property Velocity { dx: int = 0, dy: int = 0 } property Velocity { dx: int = 0, dy: int = 0 }
model Actor { Transform, Velocity } model Actor { Transform, Velocity }
@Queries(these: [Transform{scale > 0}, Velocity{dx > 0 or dy > 0}], on: Actor) @Queries(these: [Transform { scale > 0 }, Velocity { dx > 0 or dy > 0 }], on: Actor)
handler Move phase Update { handler Move phase Update {
Transform.x = Transform.x + Velocity.dx # each property is bound by its name Transform.x = Transform.x + Velocity.dx # each property is bound by its name
} }
@ -474,7 +477,7 @@ It desugars to the ordinary loop
```ludic ```ludic
# doc-check: skip — the desugaring of the @Queries above # doc-check: skip — the desugaring of the @Queries above
for (Transform, Velocity) in query [Transform, Velocity, {Actor}] for (Transform, Velocity) in query [Transform, Velocity, {Actor}]
where Transform.scale > 0 and (Velocity.dx > 0 or Velocity.dy > 0) { … } where Transform.scale > 0 and (Velocity.dx > 0 or Velocity.dy > 0) { … }
``` ```
— each listed property becomes a binding **named after itself**, a — each listed property becomes a binding **named after itself**, a
@ -529,7 +532,7 @@ boot ── @OnStart ─▶ spawn ── @OnAttach(P), @OnSpawn(M) ─▶ …
@OnSpawn(Enemy) handler Init { Health.hp = Health.max } # constructor @OnSpawn(Enemy) handler Init { Health.hp = Health.max } # constructor
@OnDespawn(Enemy) handler Clean { drop_loot(Health.hp) } # destructor @OnDespawn(Enemy) handler Clean { drop_loot(Health.hp) } # destructor
@OnDespawn(Enemy, reason: r) handler End { # destructor that knows why @OnDespawn(Enemy, reason: r) handler End { # destructor that knows why
match r { EndReason.Quit => save() ; _ => drop_loot(Health.hp) } match r { EndReason.Quit => save(); _ => drop_loot(Health.hp) }
} }
@OnAttach(Sprite) handler Load { Sprite.id = image_load("goblin.png") } @OnAttach(Sprite) handler Load { Sprite.id = image_load("goblin.png") }
@OnDetach(Sprite) handler Free { image_drop(Sprite.id) } # paired teardown @OnDetach(Sprite) handler Free { image_drop(Sprite.id) } # paired teardown
@ -628,8 +631,7 @@ emit Hurt(entity: e, amount: 5) # fires every listener
a callback and is dispatched to right after the native listeners — the closed and a callback and is dispatched to right after the native listeners — the closed and
open halves, one dispatch. Native listeners cost a direct call; foreign ones one open halves, one dispatch. Native listeners cost a direct call; foreign ones one
indirect call over a fixed-capacity array (registration order = dispatch order, indirect call over a fixed-capacity array (registration order = dispatch order,
so a modded game stays deterministic). See [`examples/mod_host.ludic`](examples/mod_host.ludic) so a modded game stays deterministic). See [`examples/events/mod_events.ludic`](examples/events/mod_events.ludic).
and the C mod in [`tests/mod_c/mod.c`](tests/mod_c/mod.c).
- **`@Public` promotes a lifecycle hook to an event, across the whole - **`@Public` promotes a lifecycle hook to an event, across the whole
architecture.** The game's own lifecycle becomes moddable with no hand-written architecture.** The game's own lifecycle becomes moddable with no hand-written
@ -745,6 +747,13 @@ handler Collect phase Update {
} }
``` ```
A slice whose contents are known up front is written as a **list literal**:
`[2, 3, 5, 7]` or `["ember", "depths"]` builds a fresh slice holding exactly those
elements. The first element fixes the element type (`[]int`, `[]string`, a
record type, …) and every later element must match it; an empty `[]` is an error
(there is nothing to infer from — use `new []T`). List literals are the natural
way to write a table of records: `let rows = [Row { … }, Row { … }]`.
Indexing works as both a value and an assignment target, and composes with Indexing works as both a value and an assignment target, and composes with
fields: `toks[i].kind = T_ID` is a single address computation. fields: `toks[i].kind = T_ID` is a single address computation.
@ -771,10 +780,9 @@ Weapon.def(damage: 14, name: "pistol", fire_rate: 9) # any order, on a
``` ```
```ludic ```ludic
extern function c_hypot(a: fixed, b: fixed) -> fixed = "hypot_fx" # bind a C symbol extern function c_hypot(a: fixed, b: fixed) -> fixed = "hypot_fx" # bind a C symbol
``` ```
`extern fn … = "symbol"` declares a foreign function and binds it to a symbol `extern function … = "symbol"` declares a foreign function and binds it to a symbol
resolved at link time; pass `-L`/`-l` to ludicc to link its library. This is how resolved at link time; pass `-L`/`-l` to ludicc to link its library. This is how
Ludic calls anything with a C ABI — including a shared library built from Ludic calls anything with a C ABI — including a shared library built from
another `.ludic` file (see `examples/library/`). another `.ludic` file (see `examples/library/`).
@ -991,16 +999,15 @@ literal; test any pointer/record/slice with `x == null` / `x != null` (an unset
# 2D draw clear(color) fill_rect(x,y,w,h,color) frame_rect(...) put_px(x,y,color) # 2D draw clear(color) fill_rect(x,y,w,h,color) frame_rect(...) put_px(x,y,color)
# draw_sprite(id,x,y) draw_sprite_scaled(id,x,y,scale) present() # draw_sprite(id,x,y) draw_sprite_scaled(id,x,y,scale) present()
# text text(x,y,str,color,scale) text_int(x,y,n,color,scale) (5x7 bitmap) # text text(x,y,str,color,scale) text_int(x,y,n,color,scale) (5x7 bitmap)
# fonts font_load(path)->id (TrueType .ttf/.ttc) # fonts Font.load(path)->id (TrueType .ttf/.ttc)
# text_ttf(font,x,y,utf8,color,px) text_w(font,utf8,px)->int text_h(font,px)->int # text_ttf(font,x,y,utf8,color,px) text_w(font,utf8,px)->int text_h(font,px)->int
# images image_load(path)->id draw_image(id,x,y) draw_image_scaled(id,x,y,w,h) # images image_load(path)->id draw_image(id,x,y) draw_image_scaled(id,x,y,w,h)
# draw_9slice(id,x,y,w,h,inset) # draw_9slice(id,x,y,w,h,inset)
# UI ui_build() ui_open(id) ui_close() ui_tick(key) ui_render() # UI Ui.build() Ui.open(id) Ui.close() Ui.tick(key) Ui.render()
# ui_clicked(id)->bool ui_set_text(id,str) ui_set_int(id,n) # Ui.clicked(id)->bool Ui.set_text(id,str)
# ui_focus(id) ui_focused()->int ui_visible(id,bool) # ui_set_int(id,n) ui_focus(id) ui_focused()->int ui_visible(id,bool) (bare only)
# assets png_load(path)->id (decodes a PNG; returns a 16x16 sprite id) # assets png_load(path)->id (decodes a PNG; returns a 16x16 sprite id)
# input key()->int (current frame's key code, 0 if none) # input Input.key()->int (current frame's key code, 0 if none)
# state reg(i)->int set_reg(i,v) (64 integer resources shared by handlers)
# entity self()->entity # entity self()->entity
# save save() load()->bool (binary snapshot of the whole ECS World) # save save() load()->bool (binary snapshot of the whole ECS World)
# control quit() print(x) (a value + newline) # control quit() print(x) (a value + newline)
@ -1035,8 +1042,8 @@ compiler flag.
The self-hosted compiler is intentionally permissive: it has no separate The self-hosted compiler is intentionally permissive: it has no separate
validation pass yet, so unknown types lower to `ptr` and call arity is not validation pass yet, so unknown types lower to `ptr` and call arity is not
checked. Diagnostics are limited to parse-level errors checked. Diagnostics are limited to parse-level errors, reported as
(`ludicc(self): parse error: …`); richer static checks (unknown identifiers, `file:line: error: message`; richer static checks (unknown identifiers,
duplicate types, unknown fields, arity) are future work. duplicate types, unknown fields, arity) are future work.
### Editors ### Editors
@ -1177,7 +1184,7 @@ When a handler's whole body is one query loop, the loop header lifts into a
```ludic ```ludic
# doc-check: skip — illustrative handler # doc-check: skip — illustrative handler
@Queries(these: [Battle{hp <= 0}, Pos], on: Foe) @Queries(these: [Battle { hp <= 0 }, Pos], on: Foe)
handler CleanBattle phase LateUpdate { despawn self() } handler CleanBattle phase LateUpdate { despawn self() }
``` ```

View file

@ -15,8 +15,9 @@ with **no C compiler in the loop**.
**No C is generated, compiled or linked in a build.** No interpreter, no **No C is generated, compiled or linked in a build.** No interpreter, no
transpiler, no C runtime: the framebuffer, sprites, PNG/DEFLATE decoding, transpiler, no C runtime: the framebuffer, sprites, PNG/DEFLATE decoding,
TrueType text, the retained UI, the registers and the RNG are all written in TrueType text, the retained UI, the registers and the RNG are all written in
Ludic (`runtime/native/*.ludic`); only the window seam — five `win_*` functions Ludic (`runtime/native/*.ludic`); only the window seam — the `win_*` functions
— is hand-written LLVM IR against the platform ABI (`runtime/native/cocoa.ll`), 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. the same floor Rust and Swift stand on.
## Backends ## Backends

View file

@ -13,7 +13,7 @@ program Hello {
# runs once per match, each property bound by name. # runs once per match, each property bound by name.
@Queries(these: [Position, Velocity]) @Queries(these: [Position, Velocity])
handler AdvancePositions phase FixedUpdate { handler AdvancePositions phase FixedUpdate {
Position.column = Position.column + Velocity.delta_x Position.column += Velocity.delta_x
Position.row = Position.row + Velocity.delta_y Position.row = Position.row + Velocity.delta_y
} }

View file

@ -11,7 +11,7 @@ program SceneDemo {
on exit { print(2) } on exit { print(2) }
layer Main { layer Main {
handler Tick phase Update { handler Tick phase Update {
counter = counter + 1 counter += 1
print(100 + counter) print(100 + counter)
if counter >= 2 { become Play } # hand off to Play if counter >= 2 { become Play } # hand off to Play
} }
@ -26,7 +26,7 @@ program SceneDemo {
on exit { print(4) } on exit { print(4) }
layer World { layer World {
handler Step phase Update { handler Step phase Update {
counter = counter + 1 counter += 1
print(200 + counter) print(200 + counter)
if counter >= 2 { quit() } if counter >= 2 { quit() }
} }