# 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 var balance: int = 0 # private: only module bank sees it export event Deposited { amount: int } function add(n: int) -> void { balance += n } export function deposit(n: int) -> void { add(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. 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. - `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 } ``` 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. ### 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. `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. ### 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. ### 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. 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 `