feat(lang): ports - port Clock { ... } in a module, bind Clock { ... } in the app

A port is a record of function values a module calls through
(Clock.now()) without naming the module that answers. A port used and
never bound, a bind missing a required member or naming one the port
lacks, and a second bind are refused; the binder must see the port, and
what it binds is checked from its own file. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 04:20:12 +03:00
parent d0ad364ad4
commit b1ccaf08bd
19 changed files with 50439 additions and 47750 deletions

View file

@ -141,6 +141,43 @@ fishing/land.ludic:4: error: fishing uses items.inv_add (items/index.ludic:3): a
- `LUDIC_VIS_REPORT=1` lists these too, as `uses: <file>:<line>: <module>.<name> used from <file>
(module <m>)`, 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.
### Types are checked before anything is emitted
Between the parse and the emitter a checker walks every function, the entry, the tests, the