feat(lang): L8 registries by declaration - registry and def

registry NAME of RECORD [as PREFIX] is a global table; def NAME key { ... } in
any file is one entry, collected in source order and filled before any code
runs. Each entry gets an index constant (PREFIX_KEY), the table PREFIX_COUNT,
and a record with a key field gets it filled and a NAME_find(key). A record
literal naming a field its record lacks is now an error everywhere; a global's
initializer is lowered as its own file's code (its errors, and what its module
may see, were whichever statement came last).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 13:34:51 +03:00
parent b0b0b62bce
commit 7e7f3ab297
15 changed files with 48231 additions and 44362 deletions

View file

@ -68,6 +68,10 @@ function emit_global_init_fn() -> void {
while i < len(prog) {
let d = prog[i]
if d.kind == N_VAR and global_needs_init_code(d) {
# an initializer is the global's own file's code: its errors, and what its module
# may see (L3), are that file's - not whichever statement was lowered last
g_err_file = d.file
g_err_line = d.line
let v = emit_expr(d.a)
let cv = coerce_code(v, d.ty) # any conversion is its own line, before the store
emit(" store "); emit(llty(d.ty)); emit(" "); emit(cv); emit(", ptr @g_"); emit(d.s); emit("\n")

View file

@ -78,6 +78,7 @@ function ck_new(e: Node) -> pointer {
if r != null and fi.kind == E_FINIT {
let ft = ck_field(r, fi.s)
if ft != null { ck_give(ft, vt, fi.a, `field {fi.s} of {e.s}`) }
if ft == null { ck_err("field", fi.a, `{gen_show(e.s)} has no field {fi.s}`) }
}
i += 1
}

View file

@ -1011,6 +1011,8 @@ function parse_one_decl() -> void {
}
if is_id("ui") { push(prog, parse_ui()); return }
if is_id("namespace") { parse_namespace(); return } # #76 namespace block
if is_id("registry") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "of") { parse_registry(); return } # L8
if is_id("def") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].kind == TK_ID) { parse_def(); return } # L8
if is_id("var") { push(prog, parse_var()); return }
if is_id("const") { push(prog, parse_const()); return }
var is_unsafe = false
@ -1506,4 +1508,5 @@ function parse_program() -> void {
if is_op("}") { break }
parse_one_decl()
}
registries_finish() # L8: each registry gets its defs
}

View file

@ -0,0 +1,112 @@
# registry.ludic — L8: registration by declaration. A registry is a global table of records,
# and each `def` is one entry, written where it belongs rather than as a call in an init
# function somebody has to remember:
#
# registry Furnishings of Furnishing as HF
# def Furnishings chair { name: "Camp chair", cost: 60, seat: true }
#
# The parser collects every def (in source order, across files) and, when the program is read,
# gives the registry its entries as its initializer, a constant per entry holding its index
# (HF_CHAIR - declaration order, so a table saved by position keeps its rule: append, never
# insert), HF_COUNT, and, when the record has a `key: string` field, fills it in and adds
# furnishings_find(key) -> int. Everything generated belongs to the registry's own file, so it
# is in the registry's module and exported only when the registry is.
var g_rg_name: []pointer = new []pointer
var g_rg_type: []pointer = new []pointer
var g_rg_prefix: []pointer = new []pointer
var g_rg_var: []Node = new []Node
var g_df_reg: []pointer = new []pointer
var g_df_key: []pointer = new []pointer
var g_df_rec: []Node = new []Node
function reg_upper(s: pointer) -> pointer {
let n = len(s)
let out = bytes(n + 1)
var i = 0
while i < n {
var c = s[i]
if c >= 'a' and c <= 'z' { c = c - 32 }
out[i] = c
i += 1
}
out[n] = 0
return out
}
function reg_lower(s: pointer) -> pointer {
let n = len(s)
let out = bytes(n + 1)
var i = 0
while i < n {
var c = s[i]
if c >= 'A' and c <= 'Z' { c = c + 32 }
out[i] = c
i += 1
}
out[n] = 0
return out
}
# registry NAME of TYPE [as PREFIX]
function parse_registry() -> void {
pi += 1
let v = node(N_VAR)
v.s = eat_id()
if not is_id("of") { perr(`registry {v.s}: say what it holds - registry {v.s} of SomeRecord`) }
pi += 1
let t = ptype()
var prefix = reg_upper(v.s)
if is_id("as") {
pi += 1
prefix = eat_id()
}
v.ty = "[]" + t
push(prog, v)
push(g_rg_name, v.s)
push(g_rg_type, t)
push(g_rg_prefix, prefix)
push(g_rg_var, v)
}
# def REGISTRY key { field: value, ... }
function parse_def() -> void {
pi += 1
let reg = eat_id()
let key = eat_id()
skipnl()
let rec = record()
push(g_df_reg, reg)
push(g_df_key, key)
push(g_df_rec, rec)
}
function reg_find(name: pointer) -> int {
var i = 0
while i < len(g_rg_name) {
if (g_rg_name[i] == name) { return i }
i += 1
}
return -1
}
function reg_rec_has(rec: Node, field: pointer) -> bool {
var i = 0
while i < len(rec.kids) {
if (rec.kids[i].s == field) { return true }
i += 1
}
return false
}
# a generated declaration: the registry's file, line and export
function reg_decl(kind: int, v: Node) -> Node {
let d = node(kind)
d.file = v.file
d.line = v.line
d.vis = v.vis
return d
}
function reg_const(v: Node, name: pointer, value: int) -> void {
let c = reg_decl(N_CONST, v)
c.s = name
c.ty = "int"
let e = node(E_INT)
e.ival = value
e.file = v.file
c.a = e
push(prog, c)
}

View file

@ -0,0 +1,99 @@
# registry_finish.ludic — L8: when the program is read, each registry gets its entries.
function registries_finish() -> void {
var d = 0
while d < len(g_df_reg) {
if reg_find(g_df_reg[d]) < 0 {
g_err_file = g_df_rec[d].file
g_err_line = g_df_rec[d].line
g_parsing = false
perr(`def {g_df_reg[d]} {g_df_key[d]}: there is no registry {g_df_reg[d]}`)
}
d += 1
}
var r = 0
while r < len(g_rg_name) {
reg_fill(r)
r += 1
}
}
function reg_fill(r: int) -> void {
let v = g_rg_var[r]
let t = g_rg_type[r]
let comp = find_comp(t)
let keyed = comp != null and field_index(comp, "key") >= 0
let lst = node(E_LIST)
lst.file = v.file
lst.line = v.line
let seen = new []pointer
var n = 0
var d = 0
while d < len(g_df_reg) {
if (g_df_reg[d] == g_rg_name[r]) {
let key = g_df_key[d]
let rec = g_df_rec[d]
var j = 0
while j < len(seen) {
if (seen[j] == key) {
g_err_file = rec.file
g_err_line = rec.line
g_parsing = false
perr(`def {g_rg_name[r]} {key} is declared twice`)
}
j += 1
}
push(seen, key)
if keyed and not reg_rec_has(rec, "key") {
let fi = node(E_FINIT)
fi.s = "key"
let ks = node(E_STR)
ks.s = key
fi.a = ks
push(rec.kids, fi)
}
let nw = node(E_NEW)
nw.s = t
nw.a = rec
nw.file = rec.file
nw.line = rec.line
push(lst.kids, nw)
reg_const(v, `{g_rg_prefix[r]}_{reg_upper(key)}`, n)
n += 1
}
d += 1
}
reg_const(v, `{g_rg_prefix[r]}_COUNT`, n)
if n == 0 {
let empty = node(E_NEW)
empty.s = v.ty
empty.file = v.file
v.a = empty
} else {
v.a = lst
}
if keyed { reg_find_fn(v) }
}
# NAME_find(key) -> int: the entry's index, or -1 - written as Ludic and read like the rest
function reg_find_fn(v: Node) -> void {
let fname = `{reg_lower(v.s)}_find`
let src = `function {fname}(key: string) -> int {{\n var i = 0\n while i < len({v.s}) {{\n if {v.s}[i].key == key {{ return i }}\n i += 1\n }}\n return -1\n}}\n`
let saved_toks = toks
let saved_pi = pi
let saved_file = g_parse_file
let saved_parsing = g_parsing
g_parse_file = v.file
g_parsing = true
lex_at(src, v.line)
pi = 0
let at = len(prog)
skipnl()
parse_one_decl()
var k = at
while k < len(prog) {
prog[k].vis = v.vis
k += 1
}
toks = saved_toks
pi = saved_pi
g_parse_file = saved_file
g_parsing = saved_parsing
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff