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:
Orkun ÇAKILKAYA 2026-09-24 02:21:09 +03:00
parent 57b66bdf47
commit 6a24b14f0e
26 changed files with 61796 additions and 51949 deletions

View file

@ -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

8
changes/generics.md Normal file
View file

@ -0,0 +1,8 @@
bump: minor
type: feature
**Generic records and functions.** `property Pool<T> { items: []T }`, `function first<T>(xs: []T)
-> T` and `function map<T, U>(xs: []T, f: fn(T) -> U) -> []U`; a type writes an instance as
`Pool<Thing>`, nested as deep as needed. A call's type arguments come from its arguments, or from
the declared type its result is written into, and are refused with the parameter named when
neither says. Each instance is compiled once as an ordinary record or function. `ludic-fmt` keeps
`Pool<Thing>` together while still spacing `a < b`.

View file

@ -0,0 +1,66 @@
# generics.ludic — L5: generic records and functions. A record takes type parameters
# (`property Pool<T>`), a function takes them after its name (`function first<T>`), and a call's
# are worked out from its arguments or, where there are none to say, from the slot its result is
# written into. Each instance is an ordinary record or function the compiler makes once.
#
# Running it prints: 7 Crater Lake a 2 west 7 0.75 2.5 1
program Generics {
numbers float
property Pool<T> {
items: []T = null
n: int = 0
}
property Pair<A, B> {
a: A
b: B
}
property Maker<T> { make: fn(int) -> T }
property Trail { name: string = "", weight: float = 0.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 pool_at<T>(p: Pool<T>, i: int) -> T { return p.items[i] }
function last_of<T>(p: Pool<T>) -> T { return pool_at(p, p.n - 1) }
function first<T>(xs: []T) -> T { return xs[0] }
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
}
function half(n: int) -> float { return float(n) / 2.0 }
function size(s: string) -> int { return len(s) }
function rope(n: int) -> Trail {
let t = new Trail
t.name = "rope"
t.weight = float(n) * 0.25
return t
}
entry {
let ints: Pool<int> = pool_new()
pool_add(ints, 3)
pool_add(ints, 4)
let trails: Pool<Trail> = pool_new()
let t = new Trail
t.name = "Crater Lake"
pool_add(trails, t)
let nested: Pool<Pool<int>> = pool_new()
pool_add(nested, ints)
let pr = new Pair<string, int>
pr.a = "west"
pr.b = 7
let m = new Maker<Trail>
m.make = fn rope
let hs = map([2, 5], fn half)
print(`{pool_at(ints, 0) + last_of(ints)} {pool_at(trails, 0).name} {first(["a", "b"])} {last_of(nested).n} {pr.a} {pr.b} {m.make(3).weight} {hs[1]} {len(map(["a"], fn size))}`)
}
}

View file

@ -0,0 +1,8 @@
# L5: a generic takes as many type arguments as it has parameters
program GenericArity {
property Pool<T> { n: int = 0 }
entry {
let p = new Pool<int, string>
print(p.n)
}
}

View file

@ -0,0 +1,9 @@
# L5: two instances of one generic are two types
program GenericMismatch {
property Pool<T> { n: int = 0 }
entry {
let ints = new Pool<int>
let words: Pool<string> = ints
print(words.n)
}
}

View file

@ -0,0 +1,9 @@
# L5: a call whose type arguments nothing decides is refused - give the result a declared type
program GenericUnbound {
property Pool<T> { n: int = 0 }
function pool_new<T>() -> Pool<T> { return new Pool<T> }
entry {
let p = pool_new()
print(p.n)
}
}

View file

@ -0,0 +1,9 @@
# L5 + L3: an instance keeps its generic's module and export - a private generic stays private
import "private_kit"
program PrivateGeneric {
entry {
let p = new Pool<int>
grow(p)
print(pool_len(p))
}
}

View file

@ -0,0 +1,2 @@
module kit
import "pool.ludic"

View file

@ -0,0 +1,11 @@
export property Pool<T> {
items: []T = null
}
function grow<T>(p: Pool<T>) -> void {
if p.items == null { p.items = new []T }
}
export function pool_add<T>(p: Pool<T>, x: T) -> void {
grow(p)
push(p.items, x)
}
export function pool_len<T>(p: Pool<T>) -> int { return len(p.items) }

View file

@ -21,8 +21,18 @@ function vis_allowed(d: Node, from: pointer) -> bool {
return module_is_friend(from)
}
# `what` is the name as written at the reference
function vis_check(d: Node, what: pointer) -> void {
# a generic function's instance is named for its type arguments (grow$int); a message names grow
function vis_plain(what: pointer) -> pointer {
var i = 0
while i < len(what) {
if what[i] == '$' { return what[0 .. i] }
i += 1
}
return what
}
function vis_check(d: Node, what0: pointer) -> void {
if d == null { return }
let what = vis_plain(what0)
var here = g_err_file
if here == null { return }
let from = module_of(here)

View file

@ -48,6 +48,7 @@ function ck_args(e: Node, name: pointer, labels: []pointer, tys: []pointer) -> v
if n != len(tys) { ck_err("arity", e, `{name} takes {itoa(len(tys))} argument(s) and this call gives {itoa(n)}`) }
var i = 0
while i < n {
if i < len(tys) { ck_expect = tys[i] }
let at = ck_expr(e.kids[i])
if i < len(tys) { ck_give(tys[i], at, e.kids[i], `argument {itoa(i + 1)} of {name}`) }
i += 1
@ -126,6 +127,8 @@ function ck_call_named(e: Node, name: pointer) -> pointer {
if bt != null { return bt }
let f = ck_fn(name)
if f != null { return ck_call_fn(e, name, f) }
let gt = gen_template(g_gen_fns, name)
if gt != null { return gen_call(e, name, gt) }
let x = ck_extern(name)
if x != null { return ck_call_fn(e, name, x) }
let g = ck_global(name)

View file

@ -29,6 +29,8 @@ function ck_any(n: Node) -> void {
}
function ck_expr(e: Node) -> pointer {
if e == null { return "?" }
let ex = ck_expect
ck_expect = null
let k = e.kind
if k == E_INT { return "int" }
if k == E_FLOAT {
@ -51,7 +53,10 @@ function ck_expr(e: Node) -> pointer {
if k == E_UN { return ck_un(e) }
if k == E_MEMBER { return ck_member(e) }
if k == E_INDEX { return ck_index_of(e) }
if k == E_CALL { return ck_call(e) }
if k == E_CALL {
ck_call_expect = ex
return ck_call(e)
}
if k == S_EMIT { ck_emit(e); return "?" }
ck_walk(e)
return "?"

View file

@ -0,0 +1,173 @@
# check_gen.ludic — L5: generic records and functions, made real. A generic declaration is taken
# out of the program before anything is checked; each instance the program names is a copy of it
# with its type parameters replaced, checked like any other declaration and put back before the
# emitter runs - so the emitter only ever sees `Pool$Thing` and `first$int`, never a `T`.
#
# A record's instances are every `Pool<...>` the program writes. A function's are found at its
# calls: the types of the arguments decide its parameters, and where they cannot (a function
# that takes no T), the slot the call is written into does - `let p: Pool<Thing> = pool_new()`.
var g_gen_recs: []Node = new []Node # the generic records, as written
var g_gen_fns: []Node = new []Node # the generic functions, as written
var g_gen_out: []Node = new []Node # the instances made, in order: spliced into prog
var g_gen_work: []Node = new []Node # function instances whose bodies still need checking
var ck_expect: pointer = null # the type the expression being checked is written into
var ck_call_expect: pointer = null # ... as it stood when the call now being checked began
function gen_template(list: []Node, name: pointer) -> Node {
var i = 0
while i < len(list) {
if (list[i].s == name) { return list[i] }
i += 1
}
return null
}
# the generic declarations leave the program; what the emitter meets is only ever an instance
function gen_collect() -> void {
let keep = new []Node
var gone_before = 0
var i = 0
while i < len(prog) {
let d = prog[i]
if (d.kind == N_COMP or d.kind == N_FN) and d.tps != null {
if d.kind == N_COMP { push(g_gen_recs, d) } else { push(g_gen_fns, d) }
if i < g_prog_user_end { gone_before += 1 }
} else {
push(keep, d)
}
i += 1
}
prog = keep
g_prog_user_end = g_prog_user_end - gone_before
}
function gen_index(xs: []pointer, s: pointer) -> int {
var i = 0
while i < len(xs) {
if (xs[i] == s) { return i }
i += 1
}
return -1
}
# a type with the parameters replaced by the arguments: T, []T, fn(T)->T, Pool<T>
function gen_subst(t: pointer, tps: []pointer, args: []pointer) -> pointer {
if t == null { return null }
let at = gen_index(tps, t)
if at >= 0 { return args[at] }
if is_slice_ty(t) { return "[]" + gen_subst(slice_elem(t), tps, args) }
if is_fn_type(t) {
let ps = fn_ty_params(t)
var out: pointer = "fn("
var i = 0
while i < len(ps) {
if i > 0 { out = out + "," }
out = out + gen_subst(ps[i], tps, args)
i += 1
}
return out + ")->" + gen_subst(fn_ty_ret(t), tps, args)
}
let gi = gen_find(t)
if gi >= 0 and g_gi_open[gi] == 1 {
let inner = gen_split(g_gi_args[gi])
let now = new []pointer
var j = 0
while j < len(inner) {
push(now, gen_subst(inner[j], tps, args))
j += 1
}
return gen_instance(g_gi_gen[gi], now)
}
return t
}
function gen_clone(n: Node, tps: []pointer, args: []pointer) -> Node {
if n == null { return null }
let c = new Node
c.kind = n.kind
c.s = n.s
if n.kind == E_NEW { c.s = gen_subst(n.s, tps, args) }
c.ival = n.ival
c.ty = gen_subst(n.ty, tps, args)
c.line = n.line
c.file = n.file
c.vis = n.vis
c.a = gen_clone(n.a, tps, args)
c.b = gen_clone(n.b, tps, args)
c.c = gen_clone(n.c, tps, args)
c.kids = new []Node
var i = 0
while i < len(n.kids) {
push(c.kids, gen_clone(n.kids[i], tps, args))
i += 1
}
return c
}
# the instance record a name stands for, made the first time it is asked for
function gen_record(name: pointer) -> Node {
let gi = gen_find(name)
if gi < 0 or g_gi_open[gi] == 1 { return null }
let t = gen_template(g_gen_recs, g_gi_gen[gi])
if t == null { return null }
let args = gen_split(g_gi_args[gi])
let tps = gen_split(t.tps)
if len(args) != len(tps) {
let at = new Node
at.file = g_gi_file[gi]
at.line = g_gi_line[gi]
ck_err("generic", at, `{t.s} takes {itoa(len(tps))} type argument(s) and {gen_show(name)} gives {itoa(len(args))}`)
return null
}
let r = gen_clone(t, tps, args)
r.s = name
push(g_gen_out, r)
ck_tab_put(ck_rec_k, ck_rec_v, name, r)
return r
}
# what a template's parameter types learn from the argument types
function gen_unify(p: pointer, a: pointer, tps: []pointer, binds: []pointer) -> void {
if p == null or ck_unknown(a) or (a == "null") { return }
let at = gen_index(tps, p)
if at >= 0 {
if binds[at] == null { binds[at] = a }
return
}
if is_slice_ty(p) and is_slice_ty(a) {
gen_unify(slice_elem(p), slice_elem(a), tps, binds)
return
}
if is_fn_type(p) and is_fn_type(a) {
let pp = fn_ty_params(p)
let ap = fn_ty_params(a)
var i = 0
while i < len(pp) and i < len(ap) {
gen_unify(pp[i], ap[i], tps, binds)
i += 1
}
gen_unify(fn_ty_ret(p), fn_ty_ret(a), tps, binds)
return
}
let pi2 = gen_find(p)
let ai = gen_find(a)
if pi2 >= 0 and ai >= 0 and (g_gi_gen[pi2] == g_gi_gen[ai]) {
let pa = gen_split(g_gi_args[pi2])
let aa = gen_split(g_gi_args[ai])
var j = 0
while j < len(pa) and j < len(aa) {
gen_unify(pa[j], aa[j], tps, binds)
j += 1
}
}
}
# Pool$Thing as the program wrote it: Pool<Thing>
function gen_show(t: pointer) -> pointer {
if t == null { return "?" }
if is_slice_ty(t) { return "[]" + gen_show(slice_elem(t)) }
let gi = gen_find(t)
if gi < 0 { return t }
let args = gen_split(g_gi_args[gi])
var out: pointer = g_gi_gen[gi] + "<"
var i = 0
while i < len(args) {
if i > 0 { out = out + ", " }
out = out + gen_show(args[i])
i += 1
}
return out + ">"
}

View file

@ -0,0 +1,132 @@
# check_gen_call.ludic — L5: a call to a generic function. Its arguments' types bind the type
# parameters (a literal only binds what nothing else did, so first(xs, 0) is decided by xs), the
# slot the call is written into binds what they cannot, and the call is rewritten to name the
# instance: `first(xs)` on a []int calls first$int, which is made and checked once however many
# calls reach it.
function gen_instance_name(name: pointer, binds: []pointer) -> pointer {
var out: pointer = name
var i = 0
while i < len(binds) {
out = out + "$" + gen_mangle(binds[i])
i += 1
}
return out
}
function gen_fn_instance(t: Node, iname: pointer, binds: []pointer) -> Node {
let have = ck_fn(iname)
if have != null { return have }
let f = gen_clone(t, gen_split(t.tps), binds)
f.s = iname
push(g_gen_out, f)
push(g_gen_work, f)
ck_tab_put(ck_fn_k, ck_fn_v, iname, f)
return f
}
# each parameter's argument node, positional or named
function gen_arg_nodes(e: Node, labels: []pointer) -> []Node {
let out = new []Node
var i = 0
while i < len(labels) {
push(out, null)
i += 1
}
var j = 0
while j < len(e.kids) {
let k = e.kids[j]
if k.kind == E_FINIT {
let at = gen_index(labels, k.s)
if at >= 0 { out[at] = k.a }
} else {
if j < len(out) { out[j] = k }
}
j += 1
}
return out
}
function gen_call(e: Node, name: pointer, t: Node) -> pointer {
let expect = ck_call_expect
let tps = gen_split(t.tps)
let labels = new []pointer
let ptys = new []pointer
ck_params(t, labels, ptys)
if len(e.kids) != len(ptys) { ck_err("arity", e, `{name} takes {itoa(len(ptys))} argument(s) and this call gives {itoa(len(e.kids))}`) }
let nodes = gen_arg_nodes(e, labels)
let atys = new []pointer
let binds = new []pointer
var i = 0
while i < len(tps) {
push(binds, null)
i += 1
}
i = 0
while i < len(nodes) {
var at: pointer = "?"
if nodes[i] != null { at = ck_expr(nodes[i]) }
push(atys, at)
if nodes[i] != null and not ck_is_lit(nodes[i]) { gen_unify(ptys[i], at, tps, binds) }
i += 1
}
gen_unify(t.ty, expect, tps, binds)
i = 0
while i < len(nodes) {
if nodes[i] != null and ck_is_lit(nodes[i]) { gen_unify(ptys[i], atys[i], tps, binds) }
i += 1
}
var k = 0
while k < len(tps) {
if binds[k] == null {
ck_err("generic", e, `cannot tell what {tps[k]} is in this call to {name}: pass it an argument that says, or write the call where its result has a declared type`)
return "?"
}
k += 1
}
let f = gen_fn_instance(t, gen_instance_name(name, binds), binds)
e.a.s = f.s
let fl = new []pointer
let fp = new []pointer
ck_params(f, fl, fp)
var j = 0
while j < len(nodes) {
if nodes[j] != null { ck_give(fp[j], atys[j], nodes[j], `argument {itoa(j + 1)} of {name}`) }
j += 1
}
return f.ty
}
# every instance a generic body asks for is made and checked in turn; then the records the
# program only ever names in a type, and the instances go back in where the program's own
# declarations end
function gen_finish() -> void {
var w = 0
while w < len(g_gen_work) {
ck_fn_body(g_gen_work[w])
w += 1
}
var r = 0
while r < len(g_gi_name) {
if g_gi_open[r] == 0 and gen_template(g_gen_recs, g_gi_gen[r]) != null { ck_record(g_gi_name[r]) }
r += 1
}
if len(g_gen_out) == 0 { return }
let np = new []Node
var i = 0
while i < len(prog) {
if i == g_prog_user_end {
var o = 0
while o < len(g_gen_out) {
push(np, g_gen_out[o])
o += 1
}
}
push(np, prog[i])
i += 1
}
if g_prog_user_end >= len(prog) {
var o2 = 0
while o2 < len(g_gen_out) {
push(np, g_gen_out[o2])
o2 += 1
}
}
prog = np
g_prog_user_end += len(g_gen_out)
}

View file

@ -69,6 +69,10 @@ function ck_index() -> void {
}
function ck_fn(name: pointer) -> Node { return ck_tab_get(ck_fn_k, ck_fn_v, name) }
function ck_global(name: pointer) -> Node { return ck_tab_get(ck_gl_k, ck_gl_v, name) }
function ck_record(name: pointer) -> Node { return ck_tab_get(ck_rec_k, ck_rec_v, name) }
function ck_record(name: pointer) -> Node {
let r = ck_tab_get(ck_rec_k, ck_rec_v, name)
if r != null { return r }
return gen_record(name)
}
function ck_enum(name: pointer) -> Node { return ck_tab_get(ck_en_k, ck_en_v, name) }
function ck_extern(name: pointer) -> Node { return ck_tab_get(ck_ex_k, ck_ex_v, name) }

View file

@ -23,6 +23,7 @@ function ck_target(t: Node) -> pointer {
}
function ck_let(s: Node) -> void {
var t: pointer = "?"
ck_expect = s.ty
if s.a != null { t = ck_expr(s.a) }
if s.ty != null {
if s.a != null { ck_give(s.ty, t, s.a, s.s) }
@ -33,6 +34,7 @@ function ck_let(s: Node) -> void {
}
function ck_assign(s: Node) -> void {
let lt = ck_target(s.a)
if (s.s == "=") { ck_expect = lt }
let rt = ck_expr(s.b)
var what: pointer = "this assignment"
if s.a.kind == E_ID { what = s.a.s }
@ -97,6 +99,7 @@ function ck_stmt(s: Node) -> void {
}
if k == S_RETURN {
if s.a == null { return }
ck_expect = ck_ret
let t = ck_expr(s.a)
if (ck_ret != "void") { ck_give(ck_ret, t, s.a, "the result") }
return
@ -132,10 +135,9 @@ function ck_listener(l: Node) -> void {
ck_block(l.a)
ck_pop(m)
}
# LUDIC_CHECK=0 turns the pass off, for bisecting a checker fault; nothing else should need it
# the pass also makes the generics real (check_gen.ludic), so it always runs
function check_program() -> void {
let off = getenv("LUDIC_CHECK")
if off != null and (off == "0") { return }
gen_collect()
ck_index()
var i = 0
while i < g_prog_user_end {
@ -150,6 +152,7 @@ function check_program() -> void {
ck_listener(g_onlisten[j])
j += 1
}
gen_finish()
if ck_errors > 0 and not ck_reporting() {
let m = `{itoa(ck_errors)} type error(s)\n`
file_write(file_stderr(), m, len(m))

View file

@ -11,7 +11,8 @@ function ck_is_int(t: pointer) -> bool {
return false
}
# "an int", "a float": the article a message puts before a type
function ck_a(t: pointer) -> pointer {
function ck_a(t0: pointer) -> pointer {
let t = gen_show(t0)
let c = t[0]
if c == 'a' or c == 'e' or c == 'i' or c == 'o' or c == 'u' or c == 'A' or c == 'E' or c == 'I' or c == 'O' or c == 'U' { return `an {t}` }
return `a {t}`

View file

@ -86,6 +86,7 @@ property Node {
line: int = 0
file: pointer = null # the source file the node was parsed from (for diagnostics)
vis: int = 0 # L3: 1 when the declaration is `export`ed from its module
tps: pointer = null # L5: a generic declaration's type parameters, "T|U"; null when not generic
}
# every node remembers where it was parsed (file + the line of the token the

View file

@ -0,0 +1,140 @@
# generics.ludic — L5: the parser's half of generic records and functions. `Pool<Thing>` in a type
# position becomes the name `Pool$Thing`, and a table remembers which generic and which arguments
# that name stands for. Every later stage sees an ordinary name - `%Cmp_Pool$Thing`,
# `@fn_first$int` are valid LLVM - and the checker (check/check_gen.ludic) makes each one real.
var g_gi_name: []pointer = new []pointer # an instance's name: Pool$Thing
var g_gi_gen: []pointer = new []pointer # the generic it is of: Pool
var g_gi_args: []pointer = new []pointer # its arguments, joined with '|': Thing
var g_gi_open: []int = new []int # 1 when an argument names a type parameter (Pool$T)
var g_gi_file: []pointer = new []pointer # where the instance was first written, for its errors
var g_gi_line: []int = new []int
var g_gen_tps: pointer = null # the type parameters of the declaration being parsed
function gen_enter(tps: pointer) -> void { g_gen_tps = tps }
# <T, U> after a declaration's name: its type parameters, "T|U"
function gen_params() -> pointer {
eat_op("<")
var out: pointer = ""
while not is_op(">") {
if len(out) > 0 { out = out + "|" }
out = out + eat_id()
if is_op(",") { pi += 1 }
}
eat_op(">")
return out
}
# a character that may stand in an instance's name: an LLVM identifier's, less '.', which the
# compiler reads as a separator in a few names of its own
function gen_name_ch(c: int) -> bool {
if c >= 'a' and c <= 'z' { return true }
if c >= 'A' and c <= 'Z' { return true }
if c >= '0' and c <= '9' { return true }
return c == '_' or c == '$'
}
function gen_mangle(t: pointer) -> pointer {
var out: pointer = ""
var i = 0
let n = len(t)
while i < n {
if t[i] == '[' and i + 1 < n and t[i + 1] == ']' {
out = out + "Sl_"
i += 2
continue
}
if gen_name_ch(t[i]) { out = out + t[i .. i + 1] } else { out = out + "_" }
i += 1
}
return out
}
function gen_split(s: pointer) -> []pointer {
let out = new []pointer
var a = 0
var i = 0
let n = len(s)
while i <= n {
if i == n or s[i] == '|' {
push(out, s[a .. i])
a = i + 1
}
i += 1
}
return out
}
function gen_is_param(name: pointer, tps: pointer) -> bool {
if tps == null { return false }
let ps = gen_split(tps)
var i = 0
while i < len(ps) {
if ps[i] == name { return true }
i += 1
}
return false
}
# does the type text mention one of the parameters as a whole name
function gen_mentions(t: pointer, tps: pointer) -> bool {
if tps == null { return false }
var a = 0
var i = 0
let n = len(t)
while i <= n {
if i == n or not (gen_name_ch(t[i]) and t[i] != '$') {
if i > a and gen_is_param(t[a .. i], tps) { return true }
a = i + 1
}
i += 1
}
return false
}
function gen_find(name: pointer) -> int {
var i = 0
while i < len(g_gi_name) {
if (g_gi_name[i] == name) { return i }
i += 1
}
return -1
}
# the instance of generic `gen` at `args`, registered on first sight
function gen_instance(gen: pointer, args: []pointer) -> pointer {
var name: pointer = gen
var joined: pointer = ""
var open = 0
var i = 0
while i < len(args) {
name = name + "$" + gen_mangle(args[i])
if i > 0 { joined = joined + "|" }
joined = joined + args[i]
if gen_mentions(args[i], g_gen_tps) { open = 1 }
if gen_find(args[i]) >= 0 and g_gi_open[gen_find(args[i])] == 1 { open = 1 }
i += 1
}
if gen_find(name) < 0 {
push(g_gi_name, name)
push(g_gi_gen, gen)
push(g_gi_args, joined)
push(g_gi_open, open)
push(g_gi_file, g_parse_file)
var ln = 0
if g_parsing and pi < len(toks) { ln = toks[pi].line }
push(g_gi_line, ln)
}
return name
}
# after the name in a type: <A, B>. `>>` closing two lists at once is split in two.
function gen_type_args(gen: pointer) -> pointer {
eat_op("<")
let args = new []pointer
while true {
if is_op(">>") {
toks[pi].text = ">"
break
}
if is_op(">") {
pi += 1
break
}
push(args, ptype())
if is_op(",") { pi += 1 }
}
return gen_instance(gen, args)
}

View file

@ -123,7 +123,9 @@ function ptype() -> pointer {
out[2 + i] = 0
return out
}
return eat_id()
let tn = eat_id()
if is_op("<") { return gen_type_args(tn) }
return tn
}
# ---- expressions -----------------------------------------------------------
@ -535,7 +537,10 @@ function is_reserved_word(w: pointer) -> bool {
}
function parse_fn() -> Node {
pi += 1; let n = node(N_FN); n.s = eat_id(); eat_op("(")
pi += 1; let n = node(N_FN); n.s = eat_id()
if is_op("<") { n.tps = gen_params() } # L5: function first<T>(xs: []T) -> T
gen_enter(n.tps)
eat_op("(")
if is_reserved_word(n.s) { perr(`'{n.s}' is a reserved word and cannot name a function`) }
while not is_op(")") {
let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); p.ty = ptype(); push(n.kids, p)
@ -545,6 +550,7 @@ function parse_fn() -> Node {
n.ty = "void"
if is_op("->") { pi += 1; n.ty = ptype() }
n.a = block()
gen_enter(null)
return n
}
function parse_main() -> Node { pi += 1; let n = node(N_MAIN); n.a = block(); return n }

View file

@ -2,7 +2,10 @@
# enum declarations, `for (vars) in query [terms] where cond`, spawn and prefab.
function parse_component() -> Node {
pi += 1; let n = node(N_COMP); n.s = eat_id(); skipnl(); eat_op("{")
pi += 1; let n = node(N_COMP); n.s = eat_id()
if is_op("<") { n.tps = gen_params() } # L5: property Pool<T> { ... }
gen_enter(n.tps)
skipnl(); eat_op("{")
while true { skipnl(); if is_op("}") { break }
var is_computed = false
var is_sync = false # @Sync — this field replicates (NETWORKING N2)
@ -13,7 +16,9 @@ function parse_component() -> Node {
if is_computed { register_computed(n.s, f.s, f.ty, f.a) } # derived: no storage
else { push(n.kids, f) }
if is_op(",") { pi += 1 } }
eat_op("}"); return n
eat_op("}")
gen_enter(null)
return n
}
# event Name { field: T = default, ... } — a public event's POD payload. Same

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -18,6 +18,7 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/frontend/lex.ludic")
push(f, "selfhost/frontend/parse.ludic")
push(f, "selfhost/frontend/parse_game.ludic")
push(f, "selfhost/frontend/generics.ludic")
push(f, "selfhost/backend/emit_core.ludic")
push(f, "selfhost/backend/emit_head.ludic")
push(f, "selfhost/backend/emit_addr.ludic")
@ -62,6 +63,8 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/check/check_ops.ludic")
push(f, "selfhost/check/check_call.ludic")
push(f, "selfhost/check/check_stmt.ludic")
push(f, "selfhost/check/check_gen.ludic")
push(f, "selfhost/check/check_gen_call.ludic")
push(f, "selfhost/backend/emit_stmt.ludic")
push(f, "selfhost/backend/game/emit_ecs.ludic")
push(f, "selfhost/backend/game/emit_query.ludic")

View file

@ -726,6 +726,11 @@ function cmd_dev_test() -> int {
reject_case("rejected/record_as_text", "argument 1 of sign wants a string and this is a Trail", "a record is not text")
reject_case("rejected/text_plus_number", "text joins text only", "text plus a number is refused")
reject_count("rejected/every_error", 3, "every type error in a program is reported at once, not only the first")
feat_case("lang/generics", "", "7 Crater Lake a 2 west 7 0.75 2.5 1", "generics.ludic (L5: generic records and functions, nested instances, fn-typed parameters and fields, inference from the result's slot)")
reject_case("rejected/generic_unbound", "cannot tell what T is in this call to pool_new", "a call whose type arguments nothing decides is refused")
reject_case("rejected/generic_mismatch", "words wants a Pool<string> and this is a Pool<int>", "two instances of one generic are two types")
reject_case("rejected/generic_arity", "Pool takes 1 type argument(s) and Pool<int, string> gives 2", "a generic takes as many type arguments as it has parameters")
reject_case("rejected/private_generic", "grow is private to module kit", "an instance of a private generic is private to its module")
# EV2 the world table: the mod reflection ABI, callable from Ludic by name.
net_case("ecs/world_get", "50 1 7")

View file

@ -87,6 +87,7 @@ program LudicFmt {
var tk_end: []int
var tk_line: []int
var linestart: []int
var tk_gen: []int # 1 on a `<`, `>` or `>>` that brackets type arguments
function ntok() -> int { return len(tk_kind) }
function tok_len(i: int) -> int { return tk_end[i] - tk_start[i] }
@ -197,6 +198,51 @@ program LudicFmt {
push_tok(LT_ERR, i, i + ln, line); i += ln
}
push_tok(LT_EOF, i, i, line)
mark_generics()
}
# ---- generics: Pool<Thing>, first<T>(, Map<string, []int> ----
function op_is(i: int, t: pointer) -> bool { return tk_kind[i] == LT_OP and (tok_text(i) == t) }
# a token that may stand inside a type argument list
function type_ish(i: int) -> bool {
let k = tk_kind[i]
if k == LT_ID or k == LT_TYPE { return true }
if k == LT_KW { return (tok_text(i) == "fn") }
if k != LT_OP { return false }
return op_is(i, ",") or op_is(i, "[") or op_is(i, "]") or op_is(i, "<") or op_is(i, ">") or op_is(i, ">>") or op_is(i, "(") or op_is(i, ")") or op_is(i, "->")
}
# a `<` written against a name, closed by a matching `>` on the same line with only a type
# between, opens type arguments; anything else is a comparison or a shift
function mark_generics() -> void {
tk_gen = new []int
var i = 0
while i < ntok() { push(tk_gen, 0); i += 1 }
i = 1
while i < ntok() {
let p = i - 1
if op_is(i, "<") and tk_gen[i] == 0 and (tk_kind[p] == LT_ID or tk_kind[p] == LT_TYPE) and tk_end[p] == tk_start[i] {
var depth = 1
var j = i + 1
var ok = true
while j < ntok() and depth > 0 and ok {
if not type_ish(j) or tk_line[j] != tk_line[i] { ok = false }
else {
if op_is(j, "<") { depth += 1 }
if op_is(j, ">") { depth -= 1 }
if op_is(j, ">>") { depth -= 2 }
if depth > 0 { j += 1 }
}
}
if ok and depth == 0 {
var m = i
while m <= j {
if op_is(m, "<") or op_is(m, ">") or op_is(m, ">>") { tk_gen[m] = 1 }
m += 1
}
}
}
i += 1
}
}
# ---- token-stream helpers ----
@ -249,6 +295,10 @@ program LudicFmt {
let p0 = src[tk_start[prev]]; let c0 = src[tk_start[cur]]
let p1 = (plen == 1); let c1 = (clen == 1)
if pk == LT_ERR or ck == LT_ERR { return tk_start[cur] - tk_end[prev] } # preserve an error token's spacing
# type arguments hug: Pool<Thing>, first<T>(xs), Pool<Pool<int>>
if tk_gen[cur] == 1 { return 0 }
if tk_gen[prev] == 1 and op_is(prev, "<") { return 0 }
if tk_gen[prev] == 1 and c1 and c0 == '(' { return 0 }
if c1 and (c0 == ')' or c0 == ']' or c0 == ',' or c0 == ':' or c0 == ';') { return 0 } # ) ] , : ;
if c1 and c0 == '.' and ck == LT_OP and dot_tight(prev) { return 0 }
if p1 and p0 == '.' and pk == LT_OP and dot_tight(cur) { return 0 }