From 583783449a062e92a48b0e187c7a6e164a84f877 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sat, 5 Sep 2026 01:12:26 +0300 Subject: [PATCH] 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 --- COMPILING.md | 28 +++++++----- CONTRIBUTING.md | 2 +- LANGUAGE.md | 77 ++++++++++++++++++--------------- README.md | 5 ++- docs/site/snippets/hero.ludic | 2 +- docs/site/snippets/scenes.ludic | 4 +- 6 files changed, 65 insertions(+), 53 deletions(-) diff --git a/COMPILING.md b/COMPILING.md index 85b6e3a2..18d88ce1 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -27,7 +27,7 @@ > > 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 -> 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 > `$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 @@ -42,10 +42,10 @@ and find your program rewritten in another language. ``` app.ludic - │ ludicc — lex, parse, check, lower (compiler/ludicc.c, - ▼ compiler/native.c) - app.ll LLVM IR: your systems, your properties, your runtime - │ IR assembler (compiler/driver.c) + │ ludicc — lex, parse, lower (selfhost/frontend/*.ludic, + ▼ selfhost/backend/*.ludic) + app.ll LLVM IR: your handlers, your properties, your runtime + │ IR assembler (selfhost/main.ludic drives $LUDIC_CC) ▼ app.o Mach-O / ELF / COFF object code │ system linker @@ -90,15 +90,16 @@ windowed and `--headless` builds today. > **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` -> 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. -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). -* A **module** gets neither. It is a library, and only its `@export fn`s become - public symbols; everything else stays private to the library. +* A program with only an **`entry`** block is a tool: it runs `entry` and exits. +* Either kind can be a library: only its `@export function`s become public + symbols; everything else stays private. ```ludic # 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. 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`, -`win_running`, `win_close` — and no compiler change. +with the same entry points — the window (`win_open`, `win_poll`, `win_present`, +`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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0167418b..d3e78483 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 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. -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. 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. diff --git a/LANGUAGE.md b/LANGUAGE.md index db24a73d..ae43c8d1 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -27,7 +27,7 @@ program Name { model ... # a named entity KIND (bundle of properties) const ... # compile-time constants 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 namespace ... # a block of functions with export / internal visibility 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 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 -(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 | property Pos { x: nope = 0 } +chronorift/world.ludic:1: error: expected expression ``` ## 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 } } 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) ``` @@ -107,7 +108,7 @@ The 5×7 bitmap `text` stays for zero-asset programs. For real typography, load TrueType font and draw UTF-8: ```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 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. ```ludic +var title_font: int = 0 + ui MainMenu { 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 - button id: NewGame text: "New Game" font: reg(R_FONT) size: 16 w: 236 - button id: Quit text: "Quit" font: reg(R_FONT) size: 16 w: 236 + label text: "CHRONO RIFT" font: title_font size: 26 fg: Color.Gold 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 } } ``` @@ -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. Widget types: `panel` (container + optional skin/bg/border), `col` / `row` (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 handlers: ```ludic handler Boot phase Start { - set_reg(R_FONT, font_load("…Arial.ttf")) - ui_build() # construct the tree (loads skins/images) - ui_open(UI_MainMenu) # make it active, focus the first button + title_font = Font.load("…Arial.ttf") + Ui.build() # construct the tree (loads skins/images) + Ui.open(UI_MainMenu) # make it active, focus the first button } handler Nav phase Update { - ui_tick(key()) # w/s move focus, space/enter activate - if ui_clicked(UI_Quit) { quit() } - ui_set_int(UI_HpLabel, hp) # poke dynamic values by id + Ui.tick(Input.key()) # w/s move focus, space/enter activate + if Ui.clicked(UI_Quit) { quit() } + 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 @@ -308,7 +311,7 @@ giving that entity — the query header lifts out of the body into an annotation ```ludic # 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() } ``` @@ -335,7 +338,7 @@ matched on their field values: ```ludic # 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 @@ -463,7 +466,7 @@ property Transform { x: int = 0, scale: int = 1 } property Velocity { dx: int = 0, dy: int = 0 } 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 { Transform.x = Transform.x + Velocity.dx # each property is bound by its name } @@ -474,7 +477,7 @@ It desugars to the ordinary loop ```ludic # doc-check: skip — the desugaring of the @Queries above 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 @@ -529,7 +532,7 @@ boot ── @OnStart ─▶ spawn ── @OnAttach(P), @OnSpawn(M) ─▶ … @OnSpawn(Enemy) handler Init { Health.hp = Health.max } # constructor @OnDespawn(Enemy) handler Clean { drop_loot(Health.hp) } # destructor @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") } @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 open halves, one dispatch. Native listeners cost a direct call; foreign ones one 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) - and the C mod in [`tests/mod_c/mod.c`](tests/mod_c/mod.c). + so a modded game stays deterministic). See [`examples/events/mod_events.ludic`](examples/events/mod_events.ludic). - **`@Public` promotes a lifecycle hook to an event, across the whole 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 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 - 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 Ludic calls anything with a C ABI — including a shared library built from 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) # 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) -# 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 # 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) -# 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_focus(id) ui_focused()->int ui_visible(id,bool) +# 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_focus(id) ui_focused()->int ui_visible(id,bool) (bare only) # assets png_load(path)->id (decodes a PNG; returns a 16x16 sprite id) -# 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) +# input Input.key()->int (current frame's key code, 0 if none) # entity self()->entity # save save() load()->bool (binary snapshot of the whole ECS World) # control quit() print(x) (a value + newline) @@ -1035,8 +1042,8 @@ compiler flag. 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 -checked. Diagnostics are limited to parse-level errors -(`ludicc(self): parse error: …`); richer static checks (unknown identifiers, +checked. Diagnostics are limited to parse-level errors, reported as +`file:line: error: message`; richer static checks (unknown identifiers, duplicate types, unknown fields, arity) are future work. ### Editors @@ -1177,7 +1184,7 @@ When a handler's whole body is one query loop, the loop header lifts into a ```ludic # 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() } ``` diff --git a/README.md b/README.md index e4b6e573..bf550cc4 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,9 @@ with **no C compiler in the loop**. **No C is generated, compiled or linked in a build.** No interpreter, no 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 — five `win_*` functions -— is hand-written LLVM IR against the platform ABI (`runtime/native/cocoa.ll`), +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 diff --git a/docs/site/snippets/hero.ludic b/docs/site/snippets/hero.ludic index cf3dd3a5..ce486ffe 100644 --- a/docs/site/snippets/hero.ludic +++ b/docs/site/snippets/hero.ludic @@ -13,7 +13,7 @@ program Hello { # runs once per match, each property bound by name. @Queries(these: [Position, Velocity]) handler AdvancePositions phase FixedUpdate { - Position.column = Position.column + Velocity.delta_x + Position.column += Velocity.delta_x Position.row = Position.row + Velocity.delta_y } diff --git a/docs/site/snippets/scenes.ludic b/docs/site/snippets/scenes.ludic index 1a6225f5..4d6c4255 100644 --- a/docs/site/snippets/scenes.ludic +++ b/docs/site/snippets/scenes.ludic @@ -11,7 +11,7 @@ program SceneDemo { on exit { print(2) } layer Main { handler Tick phase Update { - counter = counter + 1 + counter += 1 print(100 + counter) if counter >= 2 { become Play } # hand off to Play } @@ -26,7 +26,7 @@ program SceneDemo { on exit { print(4) } layer World { handler Step phase Update { - counter = counter + 1 + counter += 1 print(200 + counter) if counter >= 2 { quit() } }