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
|
||||
|
|
|
|||
8
changes/generics.md
Normal file
8
changes/generics.md
Normal 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`.
|
||||
66
examples/lang/generics.ludic
Normal file
66
examples/lang/generics.ludic
Normal 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))}`)
|
||||
}
|
||||
}
|
||||
8
examples/rejected/generic_arity.ludic
Normal file
8
examples/rejected/generic_arity.ludic
Normal 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)
|
||||
}
|
||||
}
|
||||
9
examples/rejected/generic_mismatch.ludic
Normal file
9
examples/rejected/generic_mismatch.ludic
Normal 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)
|
||||
}
|
||||
}
|
||||
9
examples/rejected/generic_unbound.ludic
Normal file
9
examples/rejected/generic_unbound.ludic
Normal 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)
|
||||
}
|
||||
}
|
||||
9
examples/rejected/private_generic.ludic
Normal file
9
examples/rejected/private_generic.ludic
Normal 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))
|
||||
}
|
||||
}
|
||||
2
examples/rejected/private_kit/index.ludic
Normal file
2
examples/rejected/private_kit/index.ludic
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
module kit
|
||||
import "pool.ludic"
|
||||
11
examples/rejected/private_kit/pool.ludic
Normal file
11
examples/rejected/private_kit/pool.ludic
Normal 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) }
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 "?"
|
||||
|
|
|
|||
173
selfhost/check/check_gen.ludic
Normal file
173
selfhost/check/check_gen.ludic
Normal 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 + ">"
|
||||
}
|
||||
132
selfhost/check/check_gen_call.ludic
Normal file
132
selfhost/check/check_gen_call.ludic
Normal 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)
|
||||
}
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
140
selfhost/frontend/generics.ludic
Normal file
140
selfhost/frontend/generics.ludic
Normal 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)
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
56506
selfhost/ludicc.seed.ll
56506
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue