feat(lang): L5 generic records and functions
property Pool<T> { ... }, function first<T>(xs: []T) -> T, map<T, U> over fn
types; a type writes an instance as Pool<Thing>, nested as deep as needed. The
parser names an instance Pool$Thing and remembers its generic and arguments; the
checker takes the generic declarations out, infers a call's type arguments from
its arguments or its result's declared slot, and makes each instance once as an
ordinary record or function, checked like any other. Errors print Pool<Thing>.
An instance keeps its generic's module and export (L3). ludic-fmt keeps type
arguments together while spacing comparisons and shifts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
57b66bdf47
commit
6a24b14f0e
26 changed files with 61796 additions and 51949 deletions
48
LANGUAGE.md
48
LANGUAGE.md
|
|
@ -139,6 +139,54 @@ everything, so it only ever reports what it can prove; the emitter keeps its own
|
|||
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<T> {
|
||||
items: []T = null
|
||||
n: int = 0
|
||||
}
|
||||
function pool_new<T>() -> Pool<T> {
|
||||
let p = new Pool<T>
|
||||
p.items = new []T
|
||||
return p
|
||||
}
|
||||
function pool_add<T>(p: Pool<T>, x: T) -> void {
|
||||
push(p.items, x)
|
||||
p.n += 1
|
||||
}
|
||||
function map<T, U>(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<string> = pool_new()
|
||||
pool_add(names, "Crater Lake")
|
||||
print(names.n)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A type names an instance with its arguments - `Pool<Thing>`, `Pair<string, int>`,
|
||||
`Pool<Pool<int>>`, `[]Pool<float>` - 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.
|
||||
|
||||
## Models (entity kinds)
|
||||
|
||||
An `model` names a *kind* of entity and the fixed set of properties it
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue