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:
parent
b0b0b62bce
commit
7e7f3ab297
15 changed files with 48231 additions and 44362 deletions
31
LANGUAGE.md
31
LANGUAGE.md
|
|
@ -215,6 +215,37 @@ way in its own files. What is still built into the compiler is the namespaces th
|
|||
`Math`, `Text`, `List`, `Vector`, `Color`, `Time`, `Date` - and the few methods that choose their
|
||||
target by an argument's type (`Audio.play` of a handle or a name).
|
||||
|
||||
### Registries (`registry`, `def`)
|
||||
|
||||
A table of records that code used to fill with calls in an init function is declared instead:
|
||||
|
||||
```ludic
|
||||
program Camp {
|
||||
property Furnishing {
|
||||
key: string = ""
|
||||
name: string = ""
|
||||
cost: int = 0
|
||||
}
|
||||
registry Furnishings of Furnishing as HF
|
||||
def Furnishings chair { name: "Camp chair", cost: 60 }
|
||||
def Furnishings crate { name: "Crate", cost: 30 }
|
||||
entry { print(Furnishings[HF_CRATE].cost + HF_COUNT) }
|
||||
}
|
||||
```
|
||||
|
||||
`registry NAME of RECORD [as PREFIX]` is a global `[]RECORD`, and every `def NAME key { ... }` is one
|
||||
entry of it - in any file, collected in source order, and in the table before any code runs. Each
|
||||
entry gets an index constant, `PREFIX_KEY` (the prefix defaults to the registry's name in capitals),
|
||||
in declaration order, and the registry a count, `PREFIX_COUNT`. When the record has a `key: string`
|
||||
field it is filled with the entry's key, and `name_find(key)` (the registry's name in lower case)
|
||||
returns its index or -1. A def's fields are checked against the record like any record literal, a
|
||||
key is declared once, and `export registry` exports the table, its constants and its lookup.
|
||||
|
||||
Because the index is the order of the defs, a table stored by position - a save, a setting - keeps
|
||||
the rule it always had: add an entry at the end, never between two. The order is the order the
|
||||
compiler reads them in, and an `import` is read where it stands: a file's imported defs come before
|
||||
the defs written after the import line.
|
||||
|
||||
### Memory is safe unless it says `unsafe`
|
||||
|
||||
The typed buffers are slices: `words(n)`, `floats(n)`, `fixeds(n)`, `doubles(n)` and
|
||||
|
|
|
|||
8
changes/registries.md
Normal file
8
changes/registries.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Registries by declaration.** `registry Furnishings of Furnishing as HF` declares a table of
|
||||
records, and each `def Furnishings chair { name: "Camp chair", cost: 60 }` - in any file - is an
|
||||
entry of it, filled before any code runs. Every entry gets an index constant (`HF_CHAIR`, in
|
||||
declaration order), the table a count (`HF_COUNT`), and a record with a `key` field gets it filled
|
||||
in and a lookup, `furnishings_find(key)`. A def's fields are checked; a record literal naming a field
|
||||
its record does not have is now an error everywhere.
|
||||
24
examples/lang/registry.ludic
Normal file
24
examples/lang/registry.ludic
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# registry.ludic — L8: registration by declaration. A registry is a table of records; each `def`
|
||||
# is one entry, written where it belongs - in any file, in source order - rather than as a call
|
||||
# in an init function. The compiler fills the table before any code runs, gives every entry an
|
||||
# index constant (the prefix and the key: HF_CRATE), a count (HF_COUNT), fills a `key` field
|
||||
# when the record has one, and adds a lookup by key (furnishings_find).
|
||||
#
|
||||
# Running it prints: 3 Crate true 2 fire_ring -1
|
||||
program Registry {
|
||||
def Furnishings bench { name: "Bench", cost: 90, seat: true }
|
||||
property Furnishing {
|
||||
key: string = ""
|
||||
name: string = ""
|
||||
cost: int = 0
|
||||
seat: bool = false
|
||||
}
|
||||
registry Furnishings of Furnishing as HF
|
||||
def Furnishings crate { name: "Crate", cost: 30 }
|
||||
def Furnishings fire_ring { name: "Fire ring", cost: 120 }
|
||||
entry {
|
||||
var seat = "false"
|
||||
if Furnishings[HF_BENCH].seat { seat = "true" }
|
||||
print(`{HF_COUNT} {Furnishings[HF_CRATE].name} {seat} {furnishings_find("fire_ring")} {Furnishings[2].key} {furnishings_find("nope")}`)
|
||||
}
|
||||
}
|
||||
10
examples/rejected/registry_field.ludic
Normal file
10
examples/rejected/registry_field.ludic
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# L8: Furnishing has no field colour
|
||||
program R {
|
||||
property Furnishing {
|
||||
key: string = ""
|
||||
cost: int = 0
|
||||
}
|
||||
registry Furnishings of Furnishing
|
||||
def Furnishings crate { colour: 3 }
|
||||
entry { print(FURNISHINGS_COUNT) }
|
||||
}
|
||||
11
examples/rejected/registry_twice.ludic
Normal file
11
examples/rejected/registry_twice.ludic
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# L8: def Furnishings crate is declared twice
|
||||
program R {
|
||||
property Furnishing {
|
||||
key: string = ""
|
||||
cost: int = 0
|
||||
}
|
||||
registry Furnishings of Furnishing
|
||||
def Furnishings crate { cost: 1 }
|
||||
def Furnishings crate { cost: 2 }
|
||||
entry { print(FURNISHINGS_COUNT) }
|
||||
}
|
||||
10
examples/rejected/registry_unknown.ludic
Normal file
10
examples/rejected/registry_unknown.ludic
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# L8: there is no registry Tables
|
||||
program R {
|
||||
property Furnishing {
|
||||
key: string = ""
|
||||
cost: int = 0
|
||||
}
|
||||
registry Furnishings of Furnishing
|
||||
def Tables x { cost: 1 }
|
||||
entry { print(FURNISHINGS_COUNT) }
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
112
selfhost/frontend/registry.ludic
Normal file
112
selfhost/frontend/registry.ludic
Normal 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)
|
||||
}
|
||||
99
selfhost/frontend/registry_finish.ludic
Normal file
99
selfhost/frontend/registry_finish.ludic
Normal 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
|
||||
}
|
||||
46137
selfhost/ludicc.seed.ll
46137
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -20,6 +20,8 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/frontend/parse_game.ludic")
|
||||
push(f, "selfhost/frontend/generics.ludic")
|
||||
push(f, "selfhost/frontend/aliases.ludic")
|
||||
push(f, "selfhost/frontend/registry.ludic")
|
||||
push(f, "selfhost/frontend/registry_finish.ludic")
|
||||
push(f, "selfhost/backend/emit_core.ludic")
|
||||
push(f, "selfhost/backend/emit_head.ludic")
|
||||
push(f, "selfhost/backend/emit_addr.ludic")
|
||||
|
|
|
|||
|
|
@ -738,6 +738,10 @@ function cmd_dev_test() -> int {
|
|||
feat_case("lang/aliases", "", "10 5 3", "aliases.ludic (L6: a namespace method declared as an alias of a function, labelled or by its parameters)")
|
||||
reject_case("rejected/alias_arity", "Trail.length takes 2 argument(s) and this call gives 1", "an alias's arguments are checked against its target")
|
||||
reject_case("rejected/alias_twice", "Trail.length is declared twice", "a namespace method is declared once")
|
||||
feat_case("lang/registry", "", "3 Crate true 2 fire_ring -1", "registry.ludic (L8: a registry filled by def declarations - index constants, a count, the key, a lookup)")
|
||||
reject_case("rejected/registry_unknown", "there is no registry Tables", "a def names a registry that exists")
|
||||
reject_case("rejected/registry_twice", "def Furnishings crate is declared twice", "a registry's key is declared once")
|
||||
reject_case("rejected/registry_field", "Furnishing has no field colour", "a def's fields are the record's")
|
||||
|
||||
# EV2 the world table: the mod reflection ABI, callable from Ludic by name.
|
||||
net_case("ecs/world_get", "50 1 7")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue