docs(api): per-symbol pages, fuzzy search, deep token linking, hover cards
All checks were successful
docs / build-and-deploy (push) Successful in 2s

Rebuild the API Reference around one page per symbol and richer, verified content.

Pages & navigation
- One HTML page per symbol (kw-*, type-*, phase-*, screen-*, fn-*, annot-*, op-*)
  instead of a single scrolling page; namespace overview pages (ns-screen …
  ns-color) and a searchable index (api.html) with client-side fuzzy search.
- Sticky-header scroll offset (scroll-margin) so a jumped-to entry/param/color is
  never hidden, plus a flash highlight on the scrolled-to target.

Deep linking in every snippet & example
- Namespace members split: `Screen`→namespace page, `fill_rectangle`→method page;
  `Color`→palette page, `Charcoal`→its swatch — separately.
- Named arguments (`width:`) link to that parameter's anchor on the method page.
- Hover any token for a summary card built from the real API data (symbols.json).

Content & coverage
- Full authoritative surface documented from the compiler: every keyword, type,
  the 6 phases (Start/Input/FixedUpdate/Update/LateUpdate/Render, each its own
  page), all 22 annotations, namespace methods with parameter docs, builtins,
  the world_* reflection ABI, networking, operators — 155 symbols.
- Longer, clearer explanations; "model"/"model instance" terminology, not "entity";
  descriptive identifiers in every example (Position{column,row}, Velocity{delta_x,
  delta_y}, Health{current,maximum}, Player/Enemy) — no Pos/Seg/x/dx.
- Accuracy fixes from compiler ground-truth: world_count() takes no arg,
  world_query_next(property, cursor) arg order, event fields bind by name; dropped
  `when` and `module` (not in the self-hosted parser).

Tooling
- inventory.json + check.py: coverage guard (every symbol has a page), duplicate-
  token guard, and broken-link guard — fail CI so docs can't drift.
- validate.py: compiles every ```ludic example against bin/ludicc (158 compile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-29 17:53:22 +03:00
parent 25f987e30d
commit 3c7ec9b016
172 changed files with 5240 additions and 895 deletions

View file

@ -5,8 +5,31 @@ category: structure
kind: keyword
tokens: const
sig: const NAME: T = value
tip: A compile-time constant.
tip: A compile-time constant, folded into the code with no storage.
order: 5
---
A compile-time constant. Folds directly into the code — no storage, no cost.
A <code>const</code> binds a name to a value known at compile time. Unlike `var`, it has no storage and cannot be reassigned — the compiler folds it directly into wherever it is used, so it costs nothing at runtime. Reach for `const` for the fixed dimensions and magic numbers of your game — grid sizes, tile pixels, tuning values — so the code reads in names instead of literals. Give constants descriptive, uppercase names, and use them everywhere the value appears so a single edit changes the whole game.
```ludic
program Board {
const GRID_WIDTH: int = 20
const GRID_HEIGHT: int = 15
const TILE_SIZE: int = 16
property Position { column: int = 0, row: int = 0 }
model Player { Position }
handler Boot phase Start {
spawn Hero { Position { column: GRID_WIDTH / 2, row: GRID_HEIGHT / 2 } }
}
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
for (position) in query [Position, {Player}] {
Screen.fill_rectangle(x: position.column * TILE_SIZE, y: position.row * TILE_SIZE, width: TILE_SIZE, height: TILE_SIZE, color: Color.Gold)
}
Screen.show()
}
}
```

View file

@ -0,0 +1,37 @@
---
id: kw-enum
name: enum
category: structure
kind: keyword
tokens: enum
sig: enum Name { A, B, C }
tip: A named set of integer constants — names for a magic-number space.
order: 50
---
An <code>enum</code> gives names to a set of related integer values so a magic-number space — a menu selection, a game mode, a machine state — reads as names instead of bare literals. Variants number themselves from `0` in declaration order, and you access one as `Name.Variant`, which is a compile-time `int` usable anywhere an int is: in `match` patterns, comparisons, and assignments. Ludic has no distinct enum runtime type yet — an enum value lives in an ordinary `int` or `var` and is saved with it — so `enum` is best understood as a readable naming layer over `int`.
```ludic
program BattleMenu {
enum Action { Attack, Guard, Item, Flee }
var current_action: int = Action.Attack
handler ChooseAction phase Input {
let pressed = Input.key()
if pressed == 'a' { current_action = Action.Attack }
if pressed == 'g' { current_action = Action.Guard }
if pressed == 'f' { current_action = Action.Flee }
}
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
match current_action {
Action.Attack => Screen.draw_text(x: 8, y: 8, text: "ATTACK", color: Color.Crimson, scale: 2)
Action.Guard => Screen.draw_text(x: 8, y: 8, text: "GUARD", color: Color.White, scale: 2)
_ => Screen.draw_text(x: 8, y: 8, text: "FLEE", color: Color.Gold, scale: 2)
}
Screen.show()
}
}
```

View file

@ -9,4 +9,23 @@ tip: Bind a name to an external C-ABI symbol — the seam for platform and libra
order: 12
---
Bind a name to an external C-ABI symbol — the seam for platform and library calls.
An <code>extern fn</code> declares a function whose body lives outside Ludic and binds it to a C-ABI symbol resolved at link time. It is the seam through which Ludic reaches anything with a C interface — a system library, a math routine, or even another `.ludic` file compiled as a `module`. You write the Ludic signature you want to call and give the real symbol name after `=`; the linker connects them (pass `-L`/`-l` to `ludicc` to point at the library). Types must match the foreign ABI, so map each parameter and the return to the right Ludic type (`int`, `fixed`, `ptr`, …).
Parameters:
- `a` — a typed argument passed straight through to the foreign symbol
```ludic
program Physics {
extern fn c_hypot(a: fixed, b: fixed) -> fixed = "hypot_fx"
property Velocity { delta_x: int = 0, delta_y: int = 0 }
model Projectile { Velocity }
handler MeasureSpeed phase Update {
for (velocity) in query [Velocity, {Projectile}] {
let speed = c_hypot(a: fx(velocity.delta_x), b: fx(velocity.delta_y))
Screen.status(str(flr(speed)))
}
}
}
```

View file

@ -5,8 +5,34 @@ category: structure
kind: keyword
tokens: fn
sig: fn name(a: T, b: T) -> R { … }
tip: A function.
tip: A function — reusable logic called positionally or with named arguments.
order: 8
---
A function. Call it positionally or with named arguments: <code>name(a: 1, b: 2)</code>.
A <code>fn</code> declares a function: a reusable block of logic with typed parameters and a return type, written `-> R` (use `-> void` for one that returns nothing). Call it positionally, `heal(2, 10)`, or with named arguments, `heal(amount: 2, maximum: 10)`, which reads more clearly at the call site and is the house style. Functions live at program scope alongside handlers and may be called from any handler; they can `spawn`, run queries, and read program-scope state. Use them to factor out logic shared by several handlers so each handler stays short.
Parameters:
- `a`, `b` — the typed inputs; pass them positionally or by name at the call site
```ludic
program Healer {
property Health { current: int = 100, maximum: int = 100 }
model Player { Health }
fn heal(amount: int, maximum: int) -> int {
let restored = amount * 2
if restored > maximum { return maximum }
return restored
}
handler Boot phase Start {
spawn Hero { Health { current: 10, maximum: 100 } }
}
handler Recover phase Update {
for (health) in query [Health, {Player}] {
health.current = heal(amount: health.current, maximum: health.maximum)
}
}
}
```

View file

@ -5,12 +5,34 @@ category: structure
kind: keyword
tokens: handler
sig: handler Name phase P { … }
tip: A block of code the engine runs every frame during phase P.
tip: A named block the engine runs each frame during phase P.
order: 3
---
A block of code the engine runs every frame during phase <code>P</code>. With a query, the body runs once per matching entity.
A <code>handler</code> is a named block of behavior that the engine runs automatically during a given `phase` — the unit that turns your data into a game. A handler with no query runs <b>once per phase tick</b>; a handler with a `@Queries` annotation (or an inline `for (…) in query […]` loop) runs its body <b>once per matching model instance</b>, with each property bound by name and `self()` giving the current instance. Handlers are registered implicitly just by being declared, and you can pause one at runtime with `disable Handler` and bring it back with `enable Handler`. Keep behavior in handlers and keep data plain in properties — that separation is the whole point.
```ludic
handler Move phase Update { … }
program Runner {
property Position { column: int = 0, row: int = 0 }
property Velocity { delta_x: int = 0, delta_y: int = 0 }
model Player { Position, Velocity }
handler Boot phase Start {
spawn Hero { Position { column: 0, row: 6 }; Velocity { delta_x: 1 } }
}
handler AdvancePositions phase Update {
for (position, velocity) in query [Position, Velocity, {Player}] {
position.column = position.column + velocity.delta_x
}
}
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
for (position) in query [Position, {Player}] {
Screen.fill_rectangle(x: position.column * 16, y: position.row * 16, width: 16, height: 16, color: Color.LimeGreen)
}
Screen.show()
}
}
```

View file

@ -5,8 +5,19 @@ category: structure
kind: keyword
tokens: import
sig: import "file.ludic"
tip: Splice another Ludic file into this program.
tip: Splice another Ludic file's declarations into this program.
order: 10
---
Splice another Ludic file into this program. Paths resolve relative to the importer; re-imports are free.
An <code>import</code> pulls the declarations of another Ludic file into this program, letting you split a game across many files instead of one giant block. The imported file is a <b>fragment</b> — bare declarations with no `program` wrapper — and its contents are spliced in as if written here. Paths resolve relative to the importing file, imports may nest, and each resolved path is include-guarded, so importing the same file twice (even through different chains) pulls it in exactly once. Diagnostics still point at the real source file, so errors in an imported fragment report that file's name and line.
```ludic
program ChronoRift {
import "chronorift/world.ludic" # properties and models
import "chronorift/combat.ludic" # the battle handlers
handler Boot phase Start {
spawn Hero { Position { column: 4, row: 4 } }
}
}
```

View file

@ -5,8 +5,27 @@ category: structure
kind: keyword
tokens: let
sig: let name = value
tip: An immutable binding, scoped to the block it appears in.
tip: An immutable binding — the default choice for a value that never changes.
order: 7
---
An immutable binding, scoped to the block it appears in.
A <code>let</code> introduces an <b>immutable</b> binding: once set, reassigning it (`name = …`) is a compile error. Reach for `let` by default and only switch to `var` when a value genuinely needs to change — it makes intent obvious and catches accidental writes. Immutability is of the <b>binding</b>, not the object it points at: a `let` that holds a property record or a slice still lets you mutate through it (`node.current = 5`), it just cannot be repointed at a different object. Bindings inside a body are locals scoped to their block.
```ludic
program Damage {
property Health { current: int = 100, maximum: int = 100 }
model Enemy { Health }
handler Boot phase Start {
spawn Grunt { Health { current: 40 } }
}
handler ApplyHit phase Update {
let incoming_damage = 12
for (health) in query [Health, {Enemy}] {
let survivor = health.current - incoming_damage
health.current = survivor
}
}
}
```

View file

@ -5,12 +5,30 @@ category: structure
kind: keyword
tokens: model
sig: model Name { PropA, PropB, … }
tip: A named bundle of properties, so an entity that always travels together is spawned by one name.
tip: A named kind of thing — a fixed bundle of properties you spawn by one name.
order: 2
---
A named bundle of properties, so an entity that always travels together is spawned by one name.
A <code>model</code> names a <b>kind</b> of thing in your game and the fixed set of properties every instance of it carries. Instead of attaching properties one by one, you `spawn` the model by name and every listed property comes with it, seeded from its defaults. A model's name doubles as a query tag: write `{Player}` inside a `query [...]` to match only instances of that model. The model itself has no fields of its own, so you never bind it to a variable — you bind its properties and filter by its tag.
```ludic
model Player { Health, Shield }
program Arena {
property Position { column: int = 0, row: int = 0 }
property Velocity { delta_x: int = 0, delta_y: int = 0 }
property Health { current: int = 100, maximum: int = 100 }
model Player { Position, Health }
model Enemy { Position, Velocity, Health }
handler Spawn phase Start {
spawn Hero { Position { column: 2, row: 8 } }
spawn Grunt { Position { column: 18, row: 8 }; Velocity { delta_x: -1 } }
}
handler AdvanceEnemies phase Update {
for (position, velocity) in query [Position, Velocity, {Enemy}] {
position.column = position.column + velocity.delta_x
}
}
}
```

View file

@ -1,12 +0,0 @@
---
id: kw-module
name: module
category: structure
kind: keyword
tokens: module
sig: module Name { @export fn … }
tip: Build a shared library of plain C-ABI symbols instead of an executable.
order: 11
---
Build a shared library of plain C-ABI symbols instead of an executable.

View file

@ -2,11 +2,45 @@
id: kw-phase
name: phase
category: structure
kind: phase
tokens: Start Input Update FixedUpdate Render
kind: keyword
tokens: phase
sig: phase Start | Input | Update | FixedUpdate | Render
tip: When a handler runs.
tip: Names which stage of the frame a handler runs in.
order: 4
---
When a handler runs. <b>Start</b> once at boot; <b>Input</b> reads the keyboard; <b>Update</b> is the per-frame step; <b>FixedUpdate</b> is the deterministic fixed-step; <b>Render</b> draws the frame.
Every handler declares a <code>phase</code> — the stage of the frame in which the engine calls it. <b>Start</b> runs once at boot, before the first frame, and is where you seed the world. Then every frame the engine runs, in order, <b>Input</b> (read the keyboard), <b>FixedUpdate</b> (the deterministic fixed-timestep step, for physics and anything that must be reproducible), <b>Update</b> (the ordinary per-frame logic), and <b>Render</b> (draw the frame, ending with `Screen.show()`). Handlers in the same phase run in declaration order, so ordering within a phase is under your control. Put drawing only in `Render`; put keyboard reads in `Input`.
```ludic
program PhaseTour {
property Position { column: int = 0, row: int = 0 }
property Velocity { delta_x: int = 0, delta_y: int = 0 }
model Player { Position, Velocity }
handler Boot phase Start {
spawn Hero { Position { column: 5, row: 5 } }
}
handler ReadKeys phase Input {
let pressed = Input.key()
for (velocity) in query [Velocity, {Player}] {
if pressed == 'd' { velocity.delta_x = 1 }
if pressed == 'a' { velocity.delta_x = -1 }
}
}
handler AdvancePositions phase Update {
for (position, velocity) in query [Position, Velocity, {Player}] {
position.column = position.column + velocity.delta_x
}
}
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
for (position) in query [Position, {Player}] {
Screen.fill_rectangle(x: position.column * 16, y: position.row * 16, width: 16, height: 16, color: Color.White)
}
Screen.show()
}
}
```

View file

@ -5,8 +5,30 @@ category: structure
kind: keyword
tokens: program
sig: program Name { … }
tip: The top-level unit.
tip: The top-level unit — one program compiles to one native game.
order: 0
---
The top-level unit. A program compiles to one native game; everything else lives inside it.
A <code>program</code> is the outermost unit of Ludic source, and everything else — properties, models, handlers, functions, constants and your named state — lives inside its braces. Each program compiles to exactly one native game (or, with a `module`, one shared library), so a project has a single top-level `program` block. The name you give it is used for the built binary and for diagnostics, and by convention matches the file. When the engine boots, it runs your `Start` handlers once and then drives the per-frame phase loop until the game quits.
```ludic
program SpaceDrift {
property Position { column: int = 0, row: int = 0 }
model Player { Position }
var score: int = 0
handler Boot phase Start {
spawn Hero { Position { column: 10, row: 8 } }
}
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
for (position) in query [Position, {Player}] {
Screen.fill_rectangle(x: position.column * 16, y: position.row * 16, width: 16, height: 16, color: Color.LimeGreen)
}
Screen.draw_number(x: 6, y: 4, value: score, color: Color.Gold, scale: 1)
Screen.show()
}
}
```

View file

@ -5,12 +5,29 @@ category: structure
kind: keyword
tokens: property
sig: property Name { field: T = default, … }
tip: A component: a named record of fields an entity can carry.
tip: A named record of typed fields — a per-model component, or a plain heap record.
order: 1
---
A component: a named record of fields an entity can carry. Fields have a type and a default.
A <code>property</code> declares a named record of typed fields, each with a default value. It is the one record keyword in Ludic, and how you <b>use</b> it decides how it is stored: list it in a `model` (or `attach` it with `spawn`) and it becomes a per-instance component held in the engine's storage and bound in queries; construct it with `new` and it becomes a plain heap record addressed by a pointer. Fields carry a type and a default, so a freshly spawned or `new`-ed property starts fully seeded. Give fields descriptive names — `column`/`row`, not `x`/`y` — because those names are what handlers read and write.
```ludic
property Pos { x: int = 0, y: int = 0 }
program Descent {
property Position { column: int = 0, row: int = 0 }
property Health { current: int = 100, maximum: int = 100 }
model Player { Position, Health }
handler Spawn phase Start {
spawn Hero { Position { column: 4, row: 4 }; Health { current: 80 } }
}
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
for (position, health) in query [Position, Health, {Player}] {
let bar_width = health.current / 4
Screen.fill_rectangle(x: position.column, y: position.row, width: bar_width, height: 4, color: Color.Crimson)
}
Screen.show()
}
}
```

View file

@ -5,8 +5,31 @@ category: structure
kind: keyword
tokens: return
sig: return value
tip: Return from a function.
tip: Hand a value back from a function and stop running it.
order: 9
---
Return from a function.
A <code>return</code> statement ends the current function and hands its result back to the caller. Its value must match the function's declared return type; in a `-> void` function you write a bare `return` (or simply let the body end) to exit early. `return` is often paired with an early guard — check a condition and return straight away — which keeps the common path unindented. Inside a handler's query loop, prefer `break` or `continue` to control the loop; `return` leaves the whole function.
```ludic
program Clamp {
property Health { current: int = 100, maximum: int = 100 }
model Player { Health }
fn clamp_current(value: int, maximum: int) -> int {
if value < 0 { return 0 }
if value > maximum { return maximum }
return value
}
handler Boot phase Start {
spawn Hero { Health { current: 250, maximum: 100 } }
}
handler Normalize phase Update {
for (health) in query [Health, {Player}] {
health.current = clamp_current(value: health.current, maximum: health.maximum)
}
}
}
```

View file

@ -0,0 +1,43 @@
---
id: kw-ui
name: ui
category: structure
kind: keyword
tokens: ui
sig: ui { panel { … } }
tip: Declare a retained widget tree as data; the engine lays it out and draws it.
order: 50
---
A <code>ui</code> block declares a <b>retained</b> widget tree as data — panels, labels and buttons — and hands layout, drawing and keyboard focus to the engine, so you describe the interface once instead of repainting it every frame. Widget types are `panel` (a container with optional skin/background/border), `col`/`row` (pure stacks), `label`, `button` (focusable), `image` and `spacer`, and their props are evaluated at build time, so `font: title_font` reads a value the program set first. Each `id: Name` mints a `UI_Name` handle you drive from handlers: call `ui_build()` then `ui_open(UI_MainMenu)` at start, `ui_tick(Input.key())` each update to move focus and activate, and `ui_clicked(UI_Name)` to react. Draw it during `Render` with `ui_render()` between `Screen.clear` and `Screen.show()`.
```ludic
program TitleScreen {
var title_font: int = 0
ui MainMenu {
panel id: Root w: 288 pad: 16 gap: 6 bg: 0x1a1a2c align: center {
label text: "CHRONO RIFT" font: title_font size: 26 fg: 0xffe060 align: center
button id: NewGame text: "New Game" font: title_font size: 16 w: 236
button id: Quit text: "Quit" font: title_font size: 16 w: 236
}
}
handler Boot phase Start {
title_font = font_load("/System/Library/Fonts/Supplemental/Arial.ttf")
ui_build()
ui_open(UI_MainMenu)
}
handler Navigate phase Update {
ui_tick(Input.key())
if ui_clicked(UI_Quit) { quit() }
}
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
ui_render()
Screen.show()
}
}
```

View file

@ -5,12 +5,27 @@ category: structure
kind: keyword
tokens: var
sig: var name: T = value
tip: A mutable binding.
tip: A mutable binding — at program scope, your game's persistent named state.
order: 6
---
A mutable binding. At program scope it is your game's persistent, named state — the modern replacement for numeric registers.
A <code>var</code> declares a <b>mutable</b> binding: unlike `let`, you may reassign it later with `=`, `+=`, `-=`, and friends. Where it appears decides its lifetime — inside a handler or function body it is a local, and at program scope it is persistent, named game state that every handler shares. Program-scope `var`s are the modern replacement for numeric registers: instead of `reg(0)`, you write `var score: int = 0` and read and write `score` by name. Use `var` for anything that genuinely changes — accumulators, the current score, a mode flag, an elapsed-frame counter — and prefer `let` for values that never change.
```ludic
var score: int = 0
program Scorekeeper {
var score: int = 0
var elapsed_frames: int = 0
handler Tick phase Update {
elapsed_frames = elapsed_frames + 1
if elapsed_frames % 60 == 0 { score = score + 1 }
}
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
Screen.draw_text(x: 6, y: 4, text: "SCORE", color: Color.White, scale: 1)
Screen.draw_number(x: 52, y: 4, value: score, color: Color.Gold, scale: 1)
Screen.show()
}
}
```