# The Ludic Language — Reference This documents the Ludic language **as actually implemented** by `compiler/ludicc.c`. Ludic is an AI-first, statically-typed, ahead-of-time compiled language for games: an ECS is built into the language, and programs compile straight to machine code. ``` program.ludic ──ludicc──▶ program.ll ──▶ program.o ──▶ native exe / shared lib (LLVM IR; no C) ``` `ludicc` lowers Ludic to **LLVM IR itself** and links the result — see [COMPILING.md](COMPILING.md) for the pipeline, `module`/`export`, and cross-targets. There is one backend: no C is generated, compiled or linked at any point, and the runtime a program calls is itself written in Ludic. ## Program structure A program is one `program` block containing declarations: ```ludic # doc-check: skip — illustrative: elided import list program Name { import ... # pull declarations in from another file property ... # a record of typed fields — a per-entity component, or a # plain `new`-allocated record; its use decides which model ... # a named entity KIND (bundle of properties) const ... # compile-time constants function ... # functions 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 } ``` ## Multi-file programs (`import`) ```ludic # doc-check: skip — paths resolve only inside the repo program ChronoRift { import "chronorift/world.ludic" # path is relative to THIS file import "chronorift/combat.ludic" } ``` `import "emberdepths/*.ludic"` imports every `.ludic` file of a directory, in name order — a game lists its modules once. `import "camp"` names a directory through its **barrel**, `camp/index.ludic`: a fragment that lists the directory's own imports (relative to itself), in the order it wants them. A directory with no `index.ludic` is an error that says so. Packages resolve the same way: `import "ludic.render3d"` reads `ludic.render3d/index.ludic` from `ludic_modules/` or the toolchain. An imported file is a **fragment**: bare 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 name the file the line really lives in, in the `file:line: error: message` shape editors already parse: ``` chronorift/world.ludic:1: error: expected expression ``` All of a program's files share one namespace, so **a name is defined once**: two functions, two `var`s or `const`s (or an `enum` and a `const`), or two `property` / `event` records with one name are an error that names both places. Declarations the runtime splices in are its own and are not checked against each other. The same holds inside a function: a `let` or `var` declares its name once per block (another block, a loop variable or a parameter may reuse it), and a function with a result type must `return` one on every path - running off the end of its body is an error, not a zero. ### Modules (`module`, `export`, `friend module`) One namespace is not the same as one room. A barrel that says `module bank` makes its directory a **module**: the barrel, every file it imports, and every file those import - until one says `module` of its own - belong to `bank`. Inside a module every name is visible as before. From anywhere else, a module's `function`, `var`, `const`, `property` or `event` is reachable only if its declaration says `export`: ```ludic # doc-check: skip — a module spans files # bank/index.ludic module bank import "ledger.ludic" # bank/ledger.ludic export state Bank { balance: int = 0 } # its fields are bank's to change: only bank's functions do export event Deposited { amount: int } function add(b: mut Bank, n: int) -> void { b.balance += n } # private: only module bank sees it export function deposit(b: mut Bank, n: int) -> void { add(b, n) emit Deposited(amount: n) } ``` A program that imports `bank` may call `deposit` and listen to `Deposited`; calling `add` is ``` visible.ludic:5: error: add is private to module bank; mark it 'export' where it is declared (bank/ledger.ludic) ``` A file in no module - the program's own file, the runtime - is public, and a package found through `ludic_modules` or the toolchain keeps its own module rather than its importer's. A program that says `friend module lab` sees every module's private names: that is for a test harness, which has to reach inside what it tests. `friend module lab of fishing, data` narrows that to the modules named: `lab` sees their private names, and every other module's exports only. `export` is a keyword before a declaration and is not the `@export` annotation, which names a C symbol. **A module's private names are its own.** A function, `var` or `const` a module does not export may share its spelling with a name in another module, in a file in no module, or in the engine's runtime: `shop` and `weather` may each have a private `seed`, and each module's code reaches its own (a local of the same name still shadows it). There is no need to prefix a package's privates. Exported names are one namespace across the program, so two modules that both `export function seed` are still refused (`function 'seed' is defined twice`). Where two names meet, the private one is compiled under its module's name (`seed$shop`), which is the spelling a message about it may show. The same holds for a `property` and an `event`: `fishing` and `hunting` may each have a private `Catch` record and a private `Landed` event with different fields, and each module's types, `new`, `emit` and `@On` reach its own (compiled as `Catch__fishing`). Two exported ones of one spelling are refused (`'Catch' is defined twice`, `event 'Landed' is defined twice`). When one of the two is a package's export the message says so (`'UiNode' is defined twice (...): ludic_ui exports it, and exported names are one namespace - rename this one, or declare it without export inside a module of your own`); a package exports only what a program uses, so ludic.ui's `UiAct` is its own and a game's may take the name. Three kinds of record stay one namespace, because other code names them by spelling: a generic record, a property that is an entity's component (a `model` names it), and the record a component or a view generates. To move an existing codebase onto modules, build it once with `LUDIC_VIS_REPORT=1`: every reference that would be refused is printed as `vis: :: . used from ` and the build goes on, so a script can add the `export`s the program already relies on. ### What a module may reach (`uses`) `export` says what a module offers; `uses` says what a module takes. A module line may name the only other modules its files reach: ```ludic # doc-check: skip — a module spans files # fishing/index.ludic module fishing uses base, data ``` From then on a reference from `fishing` into any other module is refused, even to a name that module exports: ``` fishing/land.ludic:4: error: fishing uses items.inv_add (items/index.ludic:3): add 'uses items' to fishing's module line, or take it through a port ``` - A module with no `uses` clause keeps the rule before it - anything exported - so the rule can be switched on one module at a time. Two module lines for one module add their lists together. - A package's module counts like any other: a mechanic that says `uses ludic_base` and calls into `ludic_inventory`, `ludic_ui` or the renderer is refused. A package with no `module` line of its own (`ludic.render3d`) is named for its directory - `ludic_render3d` - for this rule; its names stay public to the export rule. Only the engine's own runtime - `Value`, `Json`, `Random` and the rest, which is in no module and no package - needs no naming. - A file in no module (the program's root) is unaffected either way. - `module ludic_base uses` with nothing after it is a module that reaches no other module at all. - A friend of a module (`friend module lab`, or `friend module lab of fishing`) is not held to its `uses` for that module. - The declared graph may not go round: `module a uses b` beside `module b uses a` is refused (`the modules' uses go round in a circle: a -> b -> a`) - one of them takes the other through a port instead. - **Layers.** `module flow in layer app uses base, items` puts `flow` in layer `app`. The modules of one layer use each other freely, without naming each other, and may go round - a game's app modules (the flow, the menus, the HUD) reach each other by design. Everything outside the layer is still held to the module's `uses`, and a layered module with no `uses` may reach nothing outside its layer. A cycle is allowed only inside a layer: `items uses hud` with `hud` in layer `app` using `items` back is `items -> layer app -> items`, refused. A module is in one layer. - `LUDIC_VIS_REPORT=1` lists these too, as `uses: :: . used from (module )`, and builds. ### Ports (`port`, `bind`) A module that needs something from outside itself - the time, a save, a sound - declares a **port** instead of naming (and `uses`-ing) the module that answers. A port is a record of function values; a member with no default is required: ```ludic # doc-check: skip — a module spans files # clock/index.ludic module clock uses units export port Clock { now: fn() -> int day: fn() -> int = fn first_day } export function hour_of_day() -> int { return Clock.now() - (Clock.day() - 1) * HOURS } # app/index.ludic - where the program is put together module app uses clock bind Clock { now: fn game_hours } function game_hours() -> int { return g_hours } ``` A member that takes nothing can be bound to a variable instead: `bind Purse { money: g_money }` for a `money: fn() -> int` writes the getter (`bind_Purse_money`, returning `g_money` as it is at each call) in the bind's own file, so the one-line wrapper function is not needed. A member that takes something is refused a variable (`bind Purse: price is a fn(int)->int, and only a member that takes nothing can be bound to a variable`). Calls go through the port by name, `Clock.now()`. `clock` never names `app`, so it needs no `uses app`; the binder must be able to see the port - it is `export`ed, and a binder that says `uses` names the port's module. What the bind names (`fn game_hours`) is checked from the bind's own file. The compiler refuses: ``` app.ludic:5: error: port Clock is used here but never bound: the program has to say 'bind Clock { ... }' once, where it is put together app.ludic:9: error: bind Clock leaves out now, which has no default app.ludic:11: error: port Clock is bound twice (first at app.ludic:10) app.ludic:9: error: port Clock has no member later ``` and a member of the wrong type is a type error like any other (`field now of Clock_port wants a fn()->int and this is a fn(int)->float`). A port nobody uses may stay unbound, and so may a port whose every member has a default: unbound, it answers with its defaults - which is how a package's fallbacks ("unbound: this machine runs the world") are written. ### State: no function writes a global (`state`, `mut`) A function that changes a module variable it was not given is a hidden coupling: nothing in its signature says what it touches, and it cannot run without the whole program around it. So a module-level `var` is refused, and a module's changing data is a **state** record instead: ```ludic # doc-check: skip — a fragment state Hiker { hips: int = -1 spine: int = -1 } function hiker_bind(h: mut Hiker, sk: Skin) -> void { h.hips = skin_joint(sk, "hips") h.spine = skin_joint(sk, "spine") } function hips_of(h: Hiker) -> int { return h.hips } ``` - **One instance, which no code names.** The program holds exactly one of each `state`, made before any code runs. It reaches code only as a parameter: `h: mut Hiker` may change it, `h: Hiker` may only read it. So a function's signature is everything it reads and writes, and a test hands it a plain value. - **Read-only is checked where it is written.** Through a read-only state the compiler refuses an assignment whose target starts at it (`h.hips = 1`, `h.list[i] = x`), a `push` onto something in it, and passing it where a `mut` one is wanted (`bump changes Tally (c: mut Tally), and c is read-only here`). A reference read out of it into a local (`let l = h.list`, `let r = h.rows[0]`) is read-only too, so a write through that is refused the same way; a value read out (`let n = h.count`) is a copy and the local's own. `machine h.mode` writes its store on every `become`. `mut` is for a state parameter only. - **The runtime supplies it at the entry points** - the only code nothing in the program calls: - a body that declares it: `entry (h: mut Hiker) { ... }`, `handler Draw(h: Hiker) phase Render { ... }`, `@On(Ping) handler Heard(h: mut Hiker) { ... }`, `@OnSpawn(M) handler Made(h: mut Hiker) { ... }`, a scene's `on enter (h: mut Hiker) { ... }`, `test "name" (h: mut Hiker) { ... }`; - a retained `ui` block, which names a state's instance by the state's name: `font: Menu.title_font`; - a component, whose header names its states: `component Tally (score: mut Score) { ... }`; - a function value: `fn tick` of `function tick(h: mut Hiker, t: Tick)` is `tick` with its leading states supplied, a `fn(Tick) -> void` - so a system's functions, a port's bind and any callback a package calls are entry points without saying so; - a port member bound to a state's field, `bind Purse { money: Wallet.cash }`; - a call the compiler writes: a namespace method's target (`Weapon.def(...)` of `function weapon_def(w: mut Weapons, ...)`), an engine system, a runtime built-in. Every other call passes its states explicitly. - **Everything else module-level is immutable all the way down.** `let LIMITS: []int = [1, 2]`, a `const`, a registry: an assignment or a `push` that starts at one is refused, and so is one through a local that holds part of it (`let r = LIMITS; push(r, 3)`). - **Tests get fresh states.** Each test block starts from states made new, in its own process under `ludic test` and in the runner run directly. - The toolchain's own programs (the compiler, the CLI) are not part of this yet: they build with `ludicc --globals`, which lets a module-level `var` through. The errors: ``` counter.ludic:5: error: this assignment: c is read-only here (c: Tally); take it as c: mut Tally to change it counter.ludic:3: error: a module-level var is refused: a module's changing data is its state (state Name { ... }), passed to the functions that use it - or, if it never changes, a let counter.ludic:5: error: this assignment: LIMITS is module-level and immutable all the way down; changing data belongs in a state, passed as a mut parameter counter.ludic:3: error: x: mut int - mut is for a state parameter, and int is not a state ``` **`ludic migrate state [file|dir...] [--prune] [--runtime] [--dry-run]` moves programs there.** It compiles each program and, from the compiler's own view of every name: 1. a var nothing writes, holding a value (an int, a string, an enum...), becomes a module-level `let` where it stands; 2. each module's other vars become one state, `state State { ... }`, where the first of them was - a module's by its name (`module fishing`: `FishingState`), a directory with no module line by its path (`ludic.render3d`: `Render3dState`), the program's own file by the program's name (`program SceneDemo`: `SceneDemoState`); the fields keep their comments; 3. every reference to one is rewritten to `_st.` (`scene_demo_st.counter`), and in a `ui` block to `.`; 4. each function's states - those it touches, and those of everything it calls, to a fixed point - become its leading parameters, `mut` where it or something it calls writes; a state a run before declared read-only becomes `mut` where it is now changed; 5. each call passes them on, each entry point declares them (after any it declares already), and each component's header declares what all its members need. Give it every program at once - a directory stands for the test programs under it - and it merges their plans before it edits anything: programs that share a package agree about it, a module two of them see different files of is one state, and a path reached as `../../packages/x` is the same file as `packages/x`. A program's own file keeps a state of its own (named for the program) whatever module it says, and a `friend module`'s other files go by their directory, since each program declares that module for itself. A package's var nothing in the given programs writes stays changing data (a game may set `r3d_dem_path`); only a private one of a package's module becomes a `let`. A name the program uses that a package migrated earlier moved into its state (`cam_pos`, now `Render3dState`'s) is rewritten through that state, and a read of the runtime's own var from outside it through the runtime function that answers it (`gl_w` is `gl_width()`). A program's own module named like a package (a game's `module fishing` beside `ludic.fishing`) gets a state of its own, `FishingAppState` / `fishing_app_st`, never the package's. It writes only under the programs and directories it is given (and `runtime/` with `--runtime`): when the programs need a change in a file anywhere else - an installed package, a module imported from a neighbouring directory - it says which and changes nothing. It prints what it cannot decide (a var read in another global's initializer, a reference in generated code), for a person to finish. A later run finds the states an earlier one made and adds to them. `--runtime` moves the runtime's own vars too. gpp's packages and examples were moved with one command: ``` ludic migrate state packages packages/ludic.lab/example/plate.ludic migrate: 1804 vars into 126 states, 64 into lets; 23498 edits in 460 files ``` **`--prune` takes out what a function no longer uses:** each state parameter that neither the function nor anything it calls uses, and the argument that fills it at every call. An argument for a parameter the callee no longer has goes too - a package's verb that dropped a state leaves its callers passing one too many, and `ludic migrate state --prune ` puts them right. A reducer keeps its state, and a state declared after a plain parameter (`home_keep(r: Records, save_st: mut Save)`) is kept where it is. When ludic.base's queues began keeping their own counts, every package verb lost its `base_st` this way (`wallet_earn(wallet_st, n)`, not `wallet_earn(base_st, wallet_st, n)`): ``` ludic migrate state --prune packages packages/ludic.lab/example/plate.ludic migrate: 0 vars into 0 states, 0 into lets; 1889 edits in 132 files ``` ### Actions and reducers (`action`, `reducer`, `dispatch`) Threading states makes a function's signature say what it touches, and it shows where one function does everything: an input handler that reads the keys and then changes the world itself takes every state the world has. An action separates the two. The input says WHAT happened; each module decides what that means for its own state, and nothing else: ```ludic program Pack { state Bag { items: []int = new []int weight: int = 0 } state Log { lines: []string = new []string } action PickUp { item: int, kg: int = 1 } # what happened: a typed record reducer Bag on PickUp(b: mut Bag, a: PickUp) { # in the module that owns Bag push(b.items, a.item) b.weight += a.kg } reducer Log on PickUp(l: mut Log, a: PickUp) { push(l.lines, `picked {a.item}`) } handler Keys phase Input { if Input.key() == 'e' { dispatch PickUp { item: 7 } } # a translator: keys to actions } } ``` - **`action Name { fields }`** is a record, with defaults like any. `export action` for other modules to dispatch it. - **`reducer State on Action(s: mut State, a: Action) { ... }`** WRITES exactly its state, and takes the action last. Between the two it may declare states it only READS - `reducer Wallet on Buy(w: mut Wallet, s: Shop, a: Buy)` - supplied like its own, for what is only known inside the drain (a price another reducer just set, the map in play, where the save lives). A second state to write is refused (`reducer Pack on Buy: a reducer writes one state, and w is a mut Wallet - read it (w: Wallet), or dispatch an action Wallet's own reducer takes`), and so is a call inside it to a function that writes another. What the dispatcher knows rides in the action. Several reducers may handle one action, one per state; a reducer is not called by name. - **`dispatch Action { fields }`** queues the action, from anywhere: a handler, a function, a listener, a reducer. The queue is the runtime's, supplied like an entry point's state, so dispatching needs no state parameter. - **When the queue is drained:** at the end of every phase of the frame loop (so what the `Input` phase dispatches is reduced before `Update`); after every phase of the ludic.base system runner (`core_tick_all`); when ludic.ui has run a frame's presses (`ui_show`, `ui_press`), so a button's action is reduced before the host presents the frame, not a frame later; and wherever the program calls `drain_actions()` (an `entry` program, a test, a loop of its own - and a host that runs UI presses some other way calls it before it presents). Draining runs the actions in the order they were dispatched, and each action's reducers in the order of their states' names - never the order of imports - so the same actions make the same changes on every machine and in a replay. - **An action a reducer dispatches** is queued behind the rest and reduced in the same drain, never re-entrantly. A queue still growing after 64 rounds of that stops the program, naming the action: `actions: Ping is still being dispatched after 64 rounds of reducers - a reducer dispatches what dispatches it`. - **Events stay** for what changes no state - a sound, a notice, telemetry - and `@On` listeners run as the event is emitted. Actions are for changes. `ludic deps` reports the widest function - the most states any function or entry point of the program's own takes - and `--check` holds it as a ratchet like its other numbers (`widest_function 12` in the baseline file). A function value's states are supplied where it is called, so a step list - `let STEPS = [fn a, fn b]` walked by a function that takes nothing - hides what it touches; `widest_reach` is the most states any function can come to, through its calls, the `fn f` it writes and the globals holding fn values it reads (a registry of systems), and the report says how many of them it does not take itself. `ludic deps --widest N` lists the N functions that take the most states, each with what it reaches; `--reach N` lists them by reach. ### Types are checked before anything is emitted Between the parse and the emitter a checker walks every function, the entry, the tests, the globals' initializers and every `@On` listener, and refuses a program whose types do not agree - all of its mix-ups at once, each at its own line: ``` trip.ludic:12: error: metres wants an int and this is a float trip.ludic:14: error: area takes 2 argument(s) and this call gives 1 trip.ludic:20: error: + of a string and an int: text joins text only - write string(x) for a number 3 type error(s) ``` What it holds apart: `int`, `float`, `fixed` and `bool` (a float or a fixed into an int is `int(x)`; a float and a fixed never meet but by a literal, which takes whichever kind its slot is); text and numbers (`string(n)` or a template); one record type and another; slices of different elements; functions of different types. A call gives exactly as many arguments as there are parameters, a `return` gives the declared result, and `push` gives the slice's own element. A function names each parameter once (`add names two parameters n`). A type written in a parameter, a result or a field names a declared type (or one of the declaration's type parameters): a misspelling is refused there (`kind_of's parameter f: there is no type CharFact`), not later as a member access that makes no sense. `pointer` is untyped, as `void *` is in C: it goes wherever a reference is wanted and takes any reference, and a `[]pointer` any slice of references. Restricting what a raw pointer may reach is `unsafe`'s job, not the checker's. Bits cross between kinds by name - `as_int(x)` / `as_fixed(n)` reinterpret a word, `float_bits(f)` / `float_from_bits(i)` a float's - never by a slot's type. A name the checker cannot type (an engine namespace's arguments, a query's bindings) agrees with everything, so it only ever reports what it can prove; the emitter keeps its own checks behind it. `LUDIC_CHECK_REPORT=1` lists every mix-up by category and fails nothing, which is how an existing program is measured before it has to pass. ### Generic records and functions A record or a function can take type parameters, written after its name: ```ludic program Pools { property Pool { items: []T = null n: int = 0 } function pool_new() -> Pool { let p = new Pool p.items = new []T return p } function pool_add(p: Pool, x: T) -> void { push(p.items, x) p.n += 1 } function map(xs: []T, f: fn(T) -> U) -> []U { let out = new []U var i = 0 while i < len(xs) { push(out, f(xs[i])) i += 1 } return out } entry { let names: Pool = pool_new() pool_add(names, "Crater Lake") print(names.n) } } ``` A type names an instance with its arguments - `Pool`, `Pair`, `Pool>`, `[]Pool` - and two instances of one generic are two types. A call's type arguments are worked out from its arguments (`pool_add(names, "x")` is `pool_add` at `string`), a literal deciding only what nothing else did; a call with nothing to say it, like `pool_new()`, takes them from the slot its result is written into - a `let` with a declared type, an assignment, an argument, a `return`. Where neither decides, the call is refused and says which parameter it could not tell. Generics are compiled by instantiation: each instance the program uses is an ordinary record or function, made and checked once, so it costs exactly what writing it out by hand would. A generic nothing instantiates is not compiled at all. ### Namespaces declared in Ludic (`alias`) A namespace method can be a name for a function. Inside a `namespace` block, ```ludic program Trails { namespace Trail { export alias length(from, to) = trail_distance alias km = trail_km } function trail_distance(a: int, b: int) -> int { return b - a } function trail_km(metres: int) -> int { return metres / 1000 } entry { print(Trail.length(to: 12, from: 2) + Trail.km(metres: 5000)) } } ``` makes `Trail.length(...)` a call to `trail_distance`: the list after the method is the labels a call may name its arguments by, in the target's parameter order, and without one the target's own parameter names are the labels (`alias show() = present` takes none). The call is the target's - same arguments, same checks, same code - so a namespace costs nothing over calling the function. This is how the engine declares its own namespaces: `Http`, `Udp`, `Process`, `Json`, `Value`, `Screen`, `Input`, `Audio`, `World`, `Tiled` and the rest are `alias` blocks in `runtime/native/namespaces.ludic`, not branches in the compiler, and a package owns an API the same way in its own files. What is still built into the compiler is the namespaces that compute inline - `Math`, `Text`, `List`, `Vector`, `Color`, `Time`, `Date` - and the few methods that choose their target by an argument's type (`Audio.play` of a handle or a name). A function of the program's own that is named like the target of one of the engine's namespace methods would take that method's calls - `Random.range` is `rng_range`, so a package's `rng_range(a, b, c)` would receive every `Random.range(1, 6)`. Where the program calls that method, the function is refused (`rng_range is the engine's Random.range, which this program calls (...), and every such call would reach this function instead; choose another name`), as a function named like a compiler built-in (`run`, `exit`) or like one of the engine runtime's own functions is. So is a type - a property, record, enum, event or state - named like one the runtime declares in a file the program uses: `property PadButton` beside the runtime's `enum PadButton` used to compile and then fail in clang, where the two layouts met (`PadButton is the runtime's enum (runtime/native/input.ludic); choose another name for this property`). ### Registries (`registry`, `def`) A table of records that code used to fill with calls in an init function is declared instead: ```ludic program Camp { property Furnishing { key: string = "" name: string = "" cost: int = 0 } registry Furnishings of Furnishing as HF def Furnishings chair { name: "Camp chair", cost: 60 } def Furnishings crate { name: "Crate", cost: 30 } entry { print(Furnishings[HF_CRATE].cost + HF_COUNT) } } ``` `registry NAME of RECORD [as PREFIX]` is a global `[]RECORD`, and every `def NAME key { ... }` is one entry of it - in any file, collected in source order, and in the table before any code runs. Each entry gets an index constant, `PREFIX_KEY` (the prefix defaults to the registry's name in capitals), in declaration order, and the registry a count, `PREFIX_COUNT`. When the record has a `key: string` field it is filled with the entry's key, and `name_find(key)` (the registry's name in lower case) returns its index or -1. A def's fields are checked against the record like any record literal, a key is declared once, and `export registry` exports the table, its constants and its lookup. Because the index is the order of the defs, a table stored by position - a save, a setting - keeps the rule it always had: add an entry at the end, never between two. The order is the order the compiler reads them in, and an `import` is read where it stands: a file's imported defs come before the defs written after the import line. A registry belongs to its module, and a `def` written in another module is refused - unless the registry says `open`: ```ludic # doc-check: skip — a module spans files # core/index.ludic module core export property System { key: string = "", run: fn() -> int = null } export open registry Systems of System as SY def Systems clock { run: fn clock_run } # weather/index.ludic module weather uses core def Systems weather { run: fn weather_run } # weather_run may stay private to weather ``` A def into an open registry goes through visibility like any other reference: the registry must be exported, and a module that says `uses` names the registry's module. What the entry itself names (`fn weather_run`) is seen from the def's own module. The errors: ``` game.ludic:4: error: def Tools saw: registry Tools is not open to other modules; declare it 'open registry Tools' in module kit, or write the def there game.ludic:4: error: Tools is private to module kit; mark it 'export' where it is declared (kit/index.ludic) ``` **The index order of an open registry** is the declaring module's own entries first, in the order they are read, then every other module's: the modules in the order of their names, each one's entries in the order they are read. `SY_CLOCK` is 0 however the program imports things, and an entry from `alpha` comes before one from `zeta` even when `zeta` is imported first - so a table saved by position keeps its meaning when a barrel's imports are reordered. The rule for a saved table is still to append: a new entry goes at the end of its own module's list, and a new module whose name sorts before an existing one moves that one's entries along. A registry whose defs are all in its own module (or in no module) keeps exactly the order it always had. ### Resource files (`registry ... from`) A registry's entries can live in a data file instead of the source: ```ludic # doc-check: skip — the file it names is beside the example registry Tools of Tool as TL from "data/tools.lres" ``` ``` # tools.lres axe { name: "Axe" weight: 1.5 uses: [{ verb: "Chop", minutes: 20 }, { verb: "Split", minutes: 10 }] } lantern { name: "Lantern", weight: 0.75, uses: [] } ``` The file is a list of entries, each a key and a record, with `#` comments. It is read when the program is compiled: the registry's record is its schema, so every entry is checked as a `def` is - a field the record does not have, a string where it wants a number, a key twice - and the error names the line in the resource file. A field whose type is a record takes a bare `{ ... }`, and one typed as a list of records takes `[{ ... }, ...]`; the schema supplies the type. Values are Ludic expressions in the registry's module, so an entry refers to another table's entry by its constant. The entries are compiled in: nothing is parsed at start-up, and a build that succeeds has checked every resource it uses. The path is the project's (where the build runs), else beside the file that declares the registry. A module that extends an open registry can bring its entries from a resource file of its own: ```ludic # doc-check: skip — the file it names is beside the example import "crafting" # module crafting: export open registry Recipes of Recipe as RC def Recipes from "recipes.lres" # the game's recipes, after crafting's own ``` `def REGISTRY from "file.lres"` reads the file as `registry ... from` does - the project's path, else beside the file that says it - and checks every entry against the registry's record, an error naming the resource file's own line (`bad_recipes.lres:3: error: field minutes of Recipe wants an int and this is a string`). Its entries are defs of the module that wrote the line, so the registry must be open (and exported) to it, and they take that module's place in the order: the declaring module's entries, then each other module's by module name, and within a file, file order. ### Map-scoped tables (`@PerMap`, `@Chunked`) A registry can hold what is on a map rather than what is in the game: its rows are not compiled in, they are read when a map loads, from that map's own directory. ```ludic # doc-check: skip — its rows are files under each map's directory property PropRow { key: string = "" # the entry's key, as every registry record @Ref(Models) model: int = 0 # a literal, or any constant by name: MDL_TENT2 x: float = 0.0 @Unit("deg") yaw: float = 0.0 @Ref(Spots) home: string = "" # a row of another map table, by its key } @PerMap @ByKey registry Props of PropRow from "props.lres" @PerMap @Chunked(64) registry Instances of InstRow from "instances/{cx}_{cz}.lres" ``` The maps are the directories under the maps root, which `package.ludic` names (`maps "assets/maps"`, the default; taken relative to the package's directory): `assets/maps/maroon/props.lres`, `assets/maps/maroon/instances/12_-3.lres`. `from` is relative to the map's directory; in a `@Chunked(n)` registry, `{cx}` and `{cz}` are the chunk's integer coordinates, `floor(x / n)` and `floor(z / n)`, written as decimals (`-3`), and they go in the file's own name, not a directory above it. The files are ordinary `.lres` entries (`key { field: value, ... }`, see Resource files), read through the same open every asset takes, so a mounted pack serves them and a dev run reads the directory. A map's row may hold an `int`, a `float`, a `bool`, a `string`, a nested record, a list of any of those (not a list of lists or of `fn` values), or a `fn` value written `fn name` (resolved among the functions of the field's type the registry's module can name). An `int` field takes a literal or any `int` constant by name, a `float` field any number or number constant; the program carries its constants by name for that, when a `@PerMap` registry exists. The record's first field is `key: string`, filled from the entry's key. Rows are in file order. A `@PerMap` registry has no `as PREFIX` and no `_KEY` constants - its keys are not known when the game compiles - takes no `def`, and is never `open`. The compiler writes, in the declaring module and exported with the registry, a `state` named after it and its verbs, prefixed with the registry's name in snake case (`GroundLayers` -> `ground_layers_`): ```ludic # doc-check: skip — what the compiler writes for the two registries above state Props { rows: []PropRow, map: string, err: string, ... } props_load(st: mut Props, map: string) -> bool # //props.lres; false + st.err if missing or wrong props_clear(st: mut Props) -> void props_find(st: Props, key: string) -> int # the row's index, or -1 props_path(st: Props, rel: string) -> string # "//", for an asset a row names property InstancesChunk { cx: int, cz: int, on: bool, rows: []InstRow, ... } state Instances { chunks: []InstancesChunk, map: string, err: string, ... } instances_in(st: mut Instances, map: string, cx: int, cz: int) -> int # its slot; no file is an empty chunk, a wrong one -1 + st.err instances_out(st: mut Instances, cx: int, cz: int) -> void instances_slot(st: Instances, cx: int, cz: int) -> int # -1 when that chunk is not in instances_find(st: Instances, slot: int, key: string) -> int # a row of that slot, or -1 instances_clear(st: mut Instances) -> void instances_path(st: Instances, rel: string) -> string ``` Read `props.rows[i]` and `len(props.rows)`, `st.chunks[s].rows`. An error is `"file:line:col: what"` (`maps/alpha/props.lres:2:18: PropRow has no field colour`, `unknown constant MDL_TENT3`), and a failed load leaves the table empty, never half filled. `_find` is a hash over the row keys, rebuilt in place on every load: O(1) and allocation-free. `_in` for a chunk that is already in hands back its slot; `_in` with another map than the one the chunks came from puts every chunk out first. `_path` makes one string: it is for load time, not a frame. A row's id for co-op is `(map, key)` for a whole-map table and `(cx, cz, key)` for a chunked one - both stable, because they are data. **The table owns its rows, and everything in them.** Every row record, every list inside a row and every record in such a list is pooled: a reload of the map, or a chunk slot refilled, resets and refills them in place - the rows list and each row's lists are emptied and refilled, each record set back to its record's defaults (a template made once) - and a record is made only when a load needs more of its type than any load before it. Nothing is allocated past that high water, so a frame may call `instances_in`. Never keep a row, a row's list or a chunk's rows across a load or an `_out`: keep the key or the index, or copy the numbers. A string field, and a whole-map table's key, is interned and safe to keep. A CHUNKED table's keys are unique across its map (`t0` .. `t91842`: a row's id is `(map, key)`, and a tree moved into another chunk keeps it; `ludicc --check` refuses a key written in two chunk files, naming both), so they are NOT interned - interning every key a player walks past would fill the bounded intern table and keep them all: a slot's keys live in the slot's own buffers, rewritten when the slot is refilled. Keep such a key past `_out` with `intern(row.key)`. A record may hold a record of its own type only through a list. The reader (the runtime's `lres.ludic`) keeps its own buffers the same way: the file's bytes in one that grows only for a bigger file, its tree as parallel lists reused from file to file. `@Ref(T)` where `T` is a `@PerMap` registry goes on a `string` field holding the row's key (an `int` field is refused: "use a string key"); its schema attribute says `"scope": "map"`. **Every map is checked with the program.** `ludicc --check` (and `ludic build --check`) reads every directory under the maps root as a map and checks, with the compiler's own resource parser, each registry's file in it (each file a chunked pattern matches) against the record: the field exists, its value has the field's type, a constant it names exists, `fn name` names a function of the field's type, `@OneOf` and `@Range` hold, an `@Ref` into a game registry is in range, and an `@Ref` into a `@PerMap` registry names a row of that table in the same map (in any of its chunks; `""` is none). A key is written once in a file. Errors carry the map file's line and column, through the diagnostics as any other (`--diagnostics=json`); `--no-maps` leaves the maps alone, and no maps root is nothing to check. `ludic build --check` reads them only for the package's `entry` (the game): a partial program - a unit test, a molecule, a bake's runner - lacks the game's constants and cannot judge them, so they are left alone there unless `--maps` asks. `LUDIC_PERMAP_SRC=` appends what the compiler wrote for each table. ### Editor attributes and the schema (`@Ref`, `@Range`, ..., `ludic schema`) A field can say what an editor of the data should offer for it, and a registry how its entries may change, on the same `@` a field's `@max(64)` is written with - one or several, on the field's line or the lines above it: ```ludic # doc-check: skip — the registries it names are declared elsewhere property Tool { @Ref(Vendors) seller: int = 0 # an index into that registry: its entries are offered @OneOf(GR_) grade: int = 0 # one of the constants whose names start GR_ @OneOf(GR_GOLD, GR_SILVER) medal: int = 0 # or one of these constants @Range(0, 20.5) @Unit("kg") weight: float = 1.0 @Asset("gltf") model: string = "" # a file of that kind (any string) @Asset("png", map) density: string = "" # a path under EACH map's directory (assets/maps//...) @Color tint: int = 0 @Node(model) grip: string = "" # a node inside the glTF that `model` names @Clip(model) swing: string = "" # a clip inside it @Material(model) finish: string = "" # a material inside it @OneOf("box", "hull") shape: string = "" # a string field: one of these words @Color @Tint(TSLOT_SHELL) shell: int = 0 # a colour for that tint slot @Derived reach: float = 0.0 # worked out at boot: anything written is overwritten @Text @Multiline blurb: string = "" # read by the player (so translated), and prose @Key bind: int = 0 # a key code } @AppendOnly @ByKey registry Tools of Tool as TL from "data/tools.lres" ``` `@Unit` takes one of the canonical ASCII spellings - `m`, `m/s`, `m/s2`, `s`, `min`, `h`, `d`, `deg`, `rad`, `rad/s`, `kg`, `N`, `N.m`, `%`, `px` - so one word means one unit to an editor and a converter: the angle is `"deg"`, never `"°"`. Any other spelling is a warning naming the canonical one where there is an obvious one (`"°"`, `"degrees"` -> `deg`, `"sec"` -> `s`, `"m/s^2"` -> `m/s2`, `"Nm"` -> `N.m`), else listing them; the schema carries the list as `"units"`. They change nothing the program does. A target that no part of the program declares - the registry an `@Ref` names, the constant of an `@Tint` or a listed `@OneOf` - is a warning, and the schema marks the attribute `"unresolved": true`: a package can name the game's registry without importing it. Everything else is an error, every one reported: a target that exists but is another kind (`@Ref(Vendor)` on a record); `@OneOf` of the wrong kind for its field - on a string field the arguments are words (`@OneOf("box", "hull")`) and every registry row's value must be one of them, on any other field a prefix ending in `_` or constants; and `@Node(f)`, `@Clip(f)` or `@Material(f)` naming anything but a field `f` of the same record that is `@Asset("gltf")`, or an `@Ref` to a registry whose record has exactly one `@Asset("gltf")` field (the model is then the row's). `@Asset(kind, map)` names a file under each map's directory, not the game's root: `ludicc --check` looks for it in every map - a @PerMap row's in its own map, a game-wide row's in all of them - and refuses a map that lacks it, unless the field says `@Asset(kind, map, optional)`; the schema marks it `"scope": "map"`. A plain `@Asset(kind)` is not looked for. `ludicc app.ludic --emit-schema out.json` (or `ludic schema [file] [-o out.json]`) writes what the compiler resolved, once the types are checked and every open registry has its entries, as one JSON object with `"schema_version": 1`: - `records` - every `property`, `state` and `event`: its module, file, line and column, its doc comment (the comment lines above it, else the one ending its line), and its fields, each with its type as text, its default as written (or null), its doc, its place and its attributes (`[{"name": "Range", "args": [0, 20.5]}]`); - `registries` - every registry: its record, prefix (null for a `@PerMap` one), resource file, whether it is open, its `"scope"` (`"map"` for `@PerMap`, else `"game"`), its `"chunk"` size (or null) and, for a map-scoped one, the `"maps"` root; its own attributes, and its entries in their final index order (`{"key": "axe", "constant": "TL_AXE", "index": 0, "file": ..., "line": ..., "col": ..., "fields": [{"name", "value", "file", "line", "col"}]}`), with `contributors`: which resource file or file of `def`s brought which keys in; - `consts` - every const: its type, its value as written, its module and doc; - `functions` - every function a `fn` value can name, exported or not: its module, return type, `params` (the arguments a caller passes), `states` (what the runtime supplies), `signature`, and `fn_type` - the type a `fn` field sees, the states stripped, spelled as a field's type is (`fn(NpcPerson,float)->bool`), so matching a function to a field is comparing two strings. - `components` - every UI component (see Components): its module, place, doc, `xml` and `lss` (the template's and the stylesheet's paths, `lss` null when it has none); `props` and `state`, the instance's own fields in the order written, each with its type, its default as written (or null), its place and doc (a prop's `attributes` are `[]`: a component's members take none); `states_read`, the program's states its header names, which the runtime supplies and the template never sees; `derived`, the fields worked out each frame, with their types (an inferred one resolved); `functions` (`{"name", "params": [["i", "int"]], "result"}`) and `events` (its `on` handlers, `{"name", "params"}`) as the template calls them, the header's states and the instance stripped; and `natives`, the registered native tags its template uses as elements; - `natives` - every `ui_native` / `ui_native_input` call whose tag is a string literal: `{"tag", "via", "handler", "module", "file", "line", "col"}`, `via` the function called and `handler` the `fn` value it passes last (else null). A tag worked out at run time cannot be known and is left out. - `units` - `@Unit`'s canonical spellings. - `lang` - the text keys and the languages (see Text keys), or null without a `lang` line. Each list is sorted by name (then file and line), a registry's entries are in index order, and the paths are the ones the compiler read, so two runs over the same source write the same file. The engine's runtime is left out. `Build.schema_hash()` is a `long`: FNV-1a 64 of exactly those bytes, for the program being built, worked out once and only when the program names it - so a tool talking to a running build can tell whether it was compiled from the data in front of it. It is 0 in a release (`ludicc --release`, which `ludic bundle` passes). ### Text keys (`Key`, `k"..."`, `lang`) A text the player reads is named by a KEY, and the key's English is a language file like any other's. `package.ludic` says where the languages are and which is the source: ``` lang "assets/lang" en # the directory, and the source language: assets/lang/en.po ``` In code a key is a literal of the builtin type `Key` - `k"module.purpose"`, or `kn"..."` for a key whose text has plural forms - written against its quote (`k "x"` is a name and a string, as ever): ```ludic # doc-check: skip — tr / trf / trn are ludic.i18n's let title = tr(k"pause.resume") # tr(key: Key) -> string let day = trf(k"hud.day", txt_num(n)) # the English's holes {1}..{4}, in order let got = trn(kn"catch.count", n, fish) # {1} is the count, the rest follow ``` A `Key` is not a `string`, and a `string` is not a `Key`: giving one where the other is wanted is an error either way, and so is `+` on a key. Keys compare with `==` and `!=`, and are fields, parameters, results, list elements and registry values like any other type (`null` is none). At run time a key is its text after a marker byte - byte 1, or 2 for `kn"..."` - so `k"pause.resume"` is the string `"\x01pause.resume"`: the runtime's `tr` is a cast, and a translator knows a key from English. `string(key)` gives that marked text, for a runtime that needs it; a program itself makes text with `tr`, and a key in a template literal's hole (`` `at {k}` ``) is an error - write `trf(k"...", ...)`. `trn` takes a plural key and `tr` / `trf` refuse one, with or without a `lang` line. (A program that declares its own type named `Key` keeps it; the builtin is then out of reach.) **The data.** A registry's `@Text` field is text by derivation: its key is `..` - the registry's name in snake case (`Items` -> `items`, `GearKinds` -> `gear_kinds`), a list field adding `.` and a nested record `.` - or, with `@TextKey("steps")` on the registry, `steps..`. A `@PerMap` table's is `maps....`. When the field's type is `Key` and a row of a compiled registry gives it no value, the compiler fills in the derived key (marked, as a literal is), so code writes `tr(Items[i].name)` and the `.lres` never spells a key; a row may still name one, `name: k"items.lamp.name"`, in a resource file or a map's file alike. ```ludic # doc-check: skip — the data file is the game's property Item { key: string = "" @Text name: Key = null # items..name, filled in when the row gives none } registry Items of Item as IT from "data/items.lres" @TextKey("steps") registry StepKinds of StepKind from "steps.lres" # steps.. ``` **The templates.** A component's text is a key too: `{t('pause.resume')}`, `title="{t('pause.close')}"`, `{t('hud.day', day)}`; `t(expr)` takes a key worked out at run time (a `Key` field reaches a template as its marked text). Words outside an element with `translate="no"` are English still waiting for a key. **The checks.** With a `lang` line and its source `.po` there, the compiler holds every key to it, each diagnostic at its `file:line:col`: - a `k"..."` literal, a template's `t('...')` literal, or a key a data row names or derives that the source `.po` does not have is an **error**; so is a `kn"..."` whose entry has no `msgid_plural`; - a call of `trf` or `trn` with a key literal, and a template's `t('key', ...)`, gives as many values after the key as the English's highest hole `{n}` (`trn`'s count is `{1}`), or it is a **warning**; - **English left** - a template's words outside `translate="no"`, a text attribute's words (`title`, `label`, `hint`, `text`, `caption`, and any other whose value is not a keyword), a quoted choice inside a hole that reads as words, and a `@Text` row whose value is still English - is a **warning**, and `ludic deps` counts them as `english_left`, a ratchet like every number there; - one English under several keys (a split) wants a `#.` description on each, for a translator to tell them apart: a **warning** at the `.po`'s line. With no `lang` line nothing is checked and nothing is said (a package test uses key literals freely); a `lang` line whose source `.po` is missing checks nothing, and the first key literal says so once. The source `.po` is gettext's: `msgid` (the key), `msgid_plural` and `msgstr[n]`, `#,` flags (`fuzzy`), `#.` descriptions, `msgctxt` (read and ignored: the key is the context), strings over several lines; `#~` entries are obsolete. `ludic schema` carries a `"lang"` object (null without a `lang` line): every key used with its kind (`code`, `template`, `data`), its sites, its English, whether it is a plural, and its description; `unused` (the source's keys used nowhere), `undescribed`, `split`, and per other language in the directory its `missing`, `fuzzy` and `extra` keys. ### Default parameters, and calls that name what they change A parameter can have a default, and a call leaves out what it does not change - the last ones when it passes arguments by position, or any it does not name. A call can pass its first arguments by position and the rest by name: ```ludic program Boxes { numbers float function box(label: string, w: float = 300.0, pad: float = 8.0, bold: bool = false) -> string { return `{label} {w} {pad} {bold}` } entry { print(box("a")) # every default print(box("b", 120.0)) # the first two by position print(box("c", bold: true)) # the subject by position, a prop by name } } ``` A default is an expression written with the function and evaluated for each call that leaves it out. A call that leaves out a parameter with no default, names one the function does not have, or puts a positional argument after a named one is refused with that said. ### Components and templates (`ludic.ui`) A UI is components, and a component is three files side by side: - `Name.ludic` declares what it takes, keeps and does; - `Name.xml` is its HTML-shaped template; - `Name.lss` holds its CSS-shaped styles, which apply to its own elements only. ```ludic # doc-check: skip — a fragment of a program that imports ludic.ui component Counter { prop label: string # its parent passes it: prop step: int = 1 # with a default state count: int = 0 # the instance's own, kept while it is on screen doubled: int = count * 2 # worked out every frame function big() -> bool { return count > 3 } # callable from the template on bump() { count += step } # an event: on-click="bump()" } ``` - **Props and state** are plain names in the component's code; each mounted instance is a record of them. A prop or state is an `int`, a `float`, a `bool`, a `string` or a `Val`. A field is anything a template can read, and records and lists become objects and lists. - **The template** has one root (use `` for several). A component is used by its name as a tag. `set count = 0` in an action sets its state, and `emit close` runs what its parent passed as `on-close`. `class`, `style` and `id` on a component's tag land on its root element, styled by the parent's sheet as well as its own; when that root is itself a component, every user in turn passes theirs down (the outermost's `id` wins). The rules of all those sheets that match the root are weighed together by specificity, as one sheet's are, and a user's rule wins a tie. - **A component's names.** An event may be called anything, `set` included (`on set(v: int)`, pressed as `set(4)`; `set x = ...` is still the action). A `string` prop given a number reads it as text (`label="{3}"` is `"3"`). Two components of one name are an error that names both files. - **Compiled in.** The compiler reads the template and the styles from beside the declaration and inlines each `@import` (a path starting with `/` is from the project's root), so a missing template fails the build and nothing has to ship beside the program. `ui_reload()` reads the files again when they change on disk and keeps every instance's state. - **A screen is a component** with no props: `ui_show("Counter", null, x, y, w, h)`, or `ui_nodes("Counter", null)` for a test. - **States in the header.** A component that reads the program's data names the states it reads and changes after its name, as an entry point does: ```ludic # doc-check: skip — a fragment of a program that imports ludic.ui component Tally (score: mut Score, look: Look) { total: int = score.points function big() -> bool { return total > look.big } on add(n: int) { score.points += n } } ``` Every getter, default, function and event takes them before the instance; a member's call to another passes them on; ludic.ui's calls into the component are given the instances; and the template never sees them - `on-click="add(5)"` passes only `5`. A header state without `mut` is read-only in every member. An older, lighter bridge remains: `view Name { ... }` gives a whole template file of ``s and ``s (loaded with `ui_load`) one model and one call, and the rest of this section applies to both: ```xml The purse: ${purse} Nothing bought yet {owned} bought ``` - **Elements.** A template is HTML-shaped: - `div`, `section`, `header`, `footer`, `nav`, `main`, `article`, `aside`, `ul`, `ol`, `li` and `form` are boxes laid out in a column; `row` is one laid out in a row. - `p`, `span`, `label`, `h1`-`h6`, `strong`, `em`, `small`, `b`, `i` and `a` are text; words inside a box become a text of their own. - `button`, `img src`, `hr` and `spacer`; `scroll` is a column that scrolls. A default stylesheet, like a browser's, sizes the headings and pads the buttons. - **Attributes.** `id`, `class` (which may be bound: `class="{picked ? 'on' : ''} row"`), `style="padding: 4px; color: gold"`, `hidden`, `disabled`, `onclick` or `on-click` (also `on-press`), and any attribute a property is named after (`width="300"`). Any other attribute is kept for `[attr=value]` selectors, as HTML's are. - **The box model and flex.** Sizes are border boxes. - `padding` and `margin` take one to four lengths, or one side by name (`padding-left`). - `border` is `2px solid #ffcc00`, or `border-width` and `border-color`. - A length is `12`, `12px`, `50%`, `fit`/`auto`, `fill` or `calc()`. `width: 0` and `height: 0` are 0, not unset. - `calc()` takes `+`, `-`, `*` and `/` and brackets over `px`, `em`, `rem`, `vw`, `vh`, plain numbers and `var()`; in a `width`, `height`, `top`, `right`, `bottom` or `left` it may also hold a percentage of the room or of the containing block (`calc(50% - 10px)`). `min()`, `max()` and `clamp(least, want, most)` pick among lengths, inside `calc()` or on their own (`width: min(300px, 50vw)`); a percentage cannot be compared there. - `top`, `right`, `bottom` and `left` take a percentage of the containing block: its width for left and right, its height for top and bottom. - `flex-grow` (or `flex`) shares out the spare room along `flex-direction`, and `fill` is a share of 1. `justify-content` takes `flex-start`/`start`, `center`, `end`, `space-between`, `space-around` or `space-evenly`. - `align-items` and `align-self` take `start`, `center`, `end` or `stretch`. `flex-wrap: wrap` breaks a row into lines, and `min-`/`max-width`/`-height` bound it. - `flex-shrink` gives up room when a line's children want more than it has, each in proportion to its shrink times its size, and none below its min size, a fixed size or its content. A scroll box shrinks (and scrolls) and a box in a column shrinks as far as the scroll boxes in it let it, so a list in a column takes the room its siblings leave with no height of its own. `flex: 1 0` is the grow and the shrink. `order` rearranges a box's children without touching the tree. - `gap`, `text-align`, `display: none`, `background(-color)`, `color`, `opacity` and `font-size` are CSS's. Every property also has a short name (`w`, `h`, `pad`, `bg`, `size`, `grow`, `align`, `justify`, `self`, `dir`, `wrap`, `alpha`). Rounded corners, font weight and a few more are things the renderer does not draw, and the runtime says so rather than ignoring them. A colour is the renderer's name for one, `#rrggbb` or `#rgb`. - **Stylesheets.** Rules go in a `