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

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