feat(lang): L9 resource files - registry ... from
registry NAME of RECORD [as PREFIX] from "file.lres" fills a registry from a data
file of key { field: value } entries, read at compile time with the record as its
schema: every entry is checked like a def, errors name the resource file's line,
nested records and lists of them are bare { } / [{ }] typed by their fields, and
values are expressions in the registry's module. The entries are compiled in, so
nothing parses at start-up and a build that succeeds has validated its resources.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7e7f3ab297
commit
e543525eb7
15 changed files with 46229 additions and 44690 deletions
29
LANGUAGE.md
29
LANGUAGE.md
|
|
@ -246,6 +246,35 @@ the rule it always had: add an entry at the end, never between two. The order is
|
||||||
compiler reads them in, and an `import` is read where it stands: a file's imported defs come before
|
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.
|
the defs written after the import line.
|
||||||
|
|
||||||
|
### Resource files (`registry ... from`)
|
||||||
|
|
||||||
|
A registry's entries can live in a data file instead of the source:
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
# doc-check: skip — the file it names is beside the example
|
||||||
|
registry Tools of Tool as TL from "data/tools.lres"
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
# tools.lres
|
||||||
|
axe {
|
||||||
|
name: "Axe"
|
||||||
|
weight: 1.5
|
||||||
|
uses: [{ verb: "Chop", minutes: 20 }, { verb: "Split", minutes: 10 }]
|
||||||
|
}
|
||||||
|
lantern { name: "Lantern", weight: 0.75, uses: [] }
|
||||||
|
```
|
||||||
|
|
||||||
|
The file is a list of entries, each a key and a record, with `#` comments. It is read when the
|
||||||
|
program is compiled: the registry's record is its schema, so every entry is checked as a `def` is
|
||||||
|
- a field the record does not have, a string where it wants a number, a key twice - and the error
|
||||||
|
names the line in the resource file. A field whose type is a record takes a bare `{ ... }`, and one
|
||||||
|
typed as a list of records takes `[{ ... }, ...]`; the schema supplies the type. Values are Ludic
|
||||||
|
expressions in the registry's module, so an entry refers to another table's entry by its constant.
|
||||||
|
The entries are compiled in: nothing is parsed at start-up, and a build that succeeds has checked
|
||||||
|
every resource it uses. The path is the project's (where the build runs), else beside the file that
|
||||||
|
declares the registry.
|
||||||
|
|
||||||
### Memory is safe unless it says `unsafe`
|
### Memory is safe unless it says `unsafe`
|
||||||
|
|
||||||
The typed buffers are slices: `words(n)`, `floats(n)`, `fixeds(n)`, `doubles(n)` and
|
The typed buffers are slices: `words(n)`, `floats(n)`, `fixeds(n)`, `doubles(n)` and
|
||||||
|
|
|
||||||
8
changes/resources.md
Normal file
8
changes/resources.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**Resource files.** `registry Tools of Tool as TL from "data/tools.lres"` fills a registry from a
|
||||||
|
data file of `key { field: value }` entries. It is read at compile time with the registry's record as
|
||||||
|
its schema - a misspelt field, a wrong type or a repeated key is a compile error at the resource
|
||||||
|
file's line - and compiled in, so nothing parses at start-up. Nested records and lists of them are
|
||||||
|
bare `{ ... }` / `[{ ... }]`, typed by the schema; values are expressions, so an entry names another
|
||||||
|
table's entry by its constant.
|
||||||
11
examples/lang/data/tools.lres
Normal file
11
examples/lang/data/tools.lres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# tools.lres - the tools a camp keeps, read by resources.ludic's registry when it is compiled
|
||||||
|
axe {
|
||||||
|
name: "Axe"
|
||||||
|
weight: 1.5
|
||||||
|
uses: [{ verb: "Chop", minutes: 20 }, { verb: "Split", minutes: 10 }]
|
||||||
|
}
|
||||||
|
lantern {
|
||||||
|
name: "Lantern"
|
||||||
|
weight: 0.75
|
||||||
|
uses: []
|
||||||
|
}
|
||||||
24
examples/lang/resources.ludic
Normal file
24
examples/lang/resources.ludic
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# resources.ludic — L9: a registry whose entries live in a resource file. `from` names the file;
|
||||||
|
# it is read when the program is compiled, every entry checked against the record (the schema),
|
||||||
|
# and compiled in - so a mistake is a compile error at the resource file's line, and nothing parses
|
||||||
|
# at start-up. A field whose type is a record, or a list of them, takes a bare `{ ... }`.
|
||||||
|
#
|
||||||
|
# Running it prints: 2 Axe 2 Split 10 0 1
|
||||||
|
program Resources {
|
||||||
|
numbers float
|
||||||
|
property Use {
|
||||||
|
verb: string = ""
|
||||||
|
minutes: int = 0
|
||||||
|
}
|
||||||
|
property Tool {
|
||||||
|
key: string = ""
|
||||||
|
name: string = ""
|
||||||
|
weight: float = 0.0
|
||||||
|
uses: []Use = null
|
||||||
|
}
|
||||||
|
registry Tools of Tool as TL from "data/tools.lres"
|
||||||
|
entry {
|
||||||
|
let axe = Tools[TL_AXE]
|
||||||
|
print(`{TL_COUNT} {axe.name} {len(axe.uses)} {axe.uses[1].verb} {axe.uses[1].minutes} {len(Tools[TL_LANTERN].uses)} {tools_find("lantern")}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
4
examples/rejected/data/bad_tools.lres
Normal file
4
examples/rejected/data/bad_tools.lres
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
axe {
|
||||||
|
name: "Axe"
|
||||||
|
weight: "heavy"
|
||||||
|
}
|
||||||
11
examples/rejected/resource_field.ludic
Normal file
11
examples/rejected/resource_field.ludic
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# L9: a resource file's entries are checked against the record, at the file's own line
|
||||||
|
program ResourceField {
|
||||||
|
numbers float
|
||||||
|
property Tool {
|
||||||
|
key: string = ""
|
||||||
|
name: string = ""
|
||||||
|
weight: float = 0.0
|
||||||
|
}
|
||||||
|
registry Tools of Tool from "data/bad_tools.lres"
|
||||||
|
entry { print(TOOLS_COUNT) }
|
||||||
|
}
|
||||||
6
examples/rejected/resource_missing.ludic
Normal file
6
examples/rejected/resource_missing.ludic
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# L9: a registry's resource file must exist
|
||||||
|
program ResourceMissing {
|
||||||
|
property Tool { key: string = "" }
|
||||||
|
registry Tools of Tool from "data/no_such.lres"
|
||||||
|
entry { print(TOOLS_COUNT) }
|
||||||
|
}
|
||||||
|
|
@ -296,6 +296,7 @@ function p_primary() -> Node {
|
||||||
let n = node(E_ID); n.s = t.text; pi += 1; return n
|
let n = node(E_ID); n.s = t.text; pi += 1; return n
|
||||||
}
|
}
|
||||||
if is_op("(") { pi += 1; skipnl(); let e = expr(); skipnl(); eat_op(")"); return e }
|
if is_op("(") { pi += 1; skipnl(); let e = expr(); skipnl(); eat_op(")"); return e }
|
||||||
|
if g_res_mode and is_op("{") { return record() } # L9: a resource's nested record, typed by its field
|
||||||
perr("expected expression")
|
perr("expected expression")
|
||||||
return node(E_INT)
|
return node(E_INT)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,19 @@ function parse_registry() -> void {
|
||||||
pi += 1
|
pi += 1
|
||||||
prefix = eat_id()
|
prefix = eat_id()
|
||||||
}
|
}
|
||||||
|
var from: pointer = null
|
||||||
|
if is_id("from") {
|
||||||
|
pi += 1
|
||||||
|
from = toks[pi].text
|
||||||
|
pi += 1
|
||||||
|
}
|
||||||
v.ty = "[]" + t
|
v.ty = "[]" + t
|
||||||
push(prog, v)
|
push(prog, v)
|
||||||
push(g_rg_name, v.s)
|
push(g_rg_name, v.s)
|
||||||
push(g_rg_type, t)
|
push(g_rg_type, t)
|
||||||
push(g_rg_prefix, prefix)
|
push(g_rg_prefix, prefix)
|
||||||
push(g_rg_var, v)
|
push(g_rg_var, v)
|
||||||
|
if from != null { res_read(v, from) }
|
||||||
}
|
}
|
||||||
# def REGISTRY key { field: value, ... }
|
# def REGISTRY key { field: value, ... }
|
||||||
function parse_def() -> void {
|
function parse_def() -> void {
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ function reg_fill(r: int) -> void {
|
||||||
fi.a = ks
|
fi.a = ks
|
||||||
push(rec.kids, fi)
|
push(rec.kids, fi)
|
||||||
}
|
}
|
||||||
|
if comp != null { res_type_fields(rec, comp) } # L9: nested records take their field's type
|
||||||
let nw = node(E_NEW)
|
let nw = node(E_NEW)
|
||||||
nw.s = t
|
nw.s = t
|
||||||
nw.a = rec
|
nw.a = rec
|
||||||
|
|
|
||||||
87
selfhost/frontend/resource.ludic
Normal file
87
selfhost/frontend/resource.ludic
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# resource.ludic — L9: a registry's entries can live in a resource file.
|
||||||
|
#
|
||||||
|
# registry Items of Item as IT from "assets/data/items.lres"
|
||||||
|
#
|
||||||
|
# The file is a list of entries, each a key and a record, `rope { name: "Rope", weight: 0.4 }`, with
|
||||||
|
# `#` comments; a field whose type is a record, or a list of records, takes a bare `{ ... }` or a
|
||||||
|
# `[{ ... }, ...]`, because the schema - the registry's record - says what it is. The file is read
|
||||||
|
# when the program is: every entry is checked like a `def` (a misspelt field, a string for an int,
|
||||||
|
# a key twice) and an error names the line in the resource file. Its values are Ludic expressions
|
||||||
|
# in the registry's module, so an entry names another table's entry by its constant (VD_STORE).
|
||||||
|
# The path is the project's (the directory the build runs in), else beside the declaring file.
|
||||||
|
var g_res_mode: bool = false
|
||||||
|
|
||||||
|
function res_path(from: pointer, declaring: pointer) -> pointer {
|
||||||
|
if read_file(from) != null { return from }
|
||||||
|
return join_path(dir_of(declaring), from)
|
||||||
|
}
|
||||||
|
function res_read(v: Node, from: pointer) -> void {
|
||||||
|
let path = res_path(from, g_parse_file)
|
||||||
|
let src = read_file(path)
|
||||||
|
if src == null { perr(`registry {v.s}: cannot read the resource file {from}`) }
|
||||||
|
let saved_toks = toks
|
||||||
|
let saved_pi = pi
|
||||||
|
let saved_file = g_parse_file
|
||||||
|
let saved_parsing = g_parsing
|
||||||
|
if not (module_of(saved_file) == "") { module_set(path, module_of(saved_file)) }
|
||||||
|
if is_float_file(saved_file) and not is_float_file(path) { push(g_float_files, path) }
|
||||||
|
g_parse_file = path
|
||||||
|
g_parsing = true
|
||||||
|
g_res_mode = true
|
||||||
|
lex_at(src, 1)
|
||||||
|
pi = 0
|
||||||
|
while true {
|
||||||
|
skipnl()
|
||||||
|
if toks[pi].kind == TK_EOF { break }
|
||||||
|
let key = eat_id()
|
||||||
|
skipnl()
|
||||||
|
let rec = record()
|
||||||
|
push(g_df_reg, v.s)
|
||||||
|
push(g_df_key, key)
|
||||||
|
push(g_df_rec, rec)
|
||||||
|
}
|
||||||
|
g_res_mode = false
|
||||||
|
toks = saved_toks
|
||||||
|
pi = saved_pi
|
||||||
|
g_parse_file = saved_file
|
||||||
|
g_parsing = saved_parsing
|
||||||
|
}
|
||||||
|
# a bare record, or a list of them, becomes a `new` of the type its field declares
|
||||||
|
function res_type(e: Node, ty: pointer) -> Node {
|
||||||
|
if e == null or ty == null { return e }
|
||||||
|
if e.kind == E_REC {
|
||||||
|
let comp = find_comp(ty)
|
||||||
|
if comp == null { return e }
|
||||||
|
res_type_fields(e, comp)
|
||||||
|
let nw = node(E_NEW)
|
||||||
|
nw.s = ty
|
||||||
|
nw.a = e
|
||||||
|
nw.file = e.file
|
||||||
|
nw.line = e.line
|
||||||
|
return nw
|
||||||
|
}
|
||||||
|
if e.kind == E_LIST and is_slice_ty(ty) {
|
||||||
|
if len(e.kids) == 0 {
|
||||||
|
let empty = node(E_NEW)
|
||||||
|
empty.s = ty
|
||||||
|
empty.file = e.file
|
||||||
|
empty.line = e.line
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
var i = 0
|
||||||
|
while i < len(e.kids) {
|
||||||
|
e.kids[i] = res_type(e.kids[i], slice_elem(ty))
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
function res_type_fields(rec: Node, comp: Node) -> void {
|
||||||
|
var i = 0
|
||||||
|
while i < len(rec.kids) {
|
||||||
|
let fi = rec.kids[i]
|
||||||
|
let fx = field_index(comp, fi.s)
|
||||||
|
if fx >= 0 { fi.a = res_type(fi.a, comp.kids[fx].ty) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
45363
selfhost/ludicc.seed.ll
45363
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -22,6 +22,7 @@ function selfhost_frags() -> []pointer {
|
||||||
push(f, "selfhost/frontend/aliases.ludic")
|
push(f, "selfhost/frontend/aliases.ludic")
|
||||||
push(f, "selfhost/frontend/registry.ludic")
|
push(f, "selfhost/frontend/registry.ludic")
|
||||||
push(f, "selfhost/frontend/registry_finish.ludic")
|
push(f, "selfhost/frontend/registry_finish.ludic")
|
||||||
|
push(f, "selfhost/frontend/resource.ludic")
|
||||||
push(f, "selfhost/backend/emit_core.ludic")
|
push(f, "selfhost/backend/emit_core.ludic")
|
||||||
push(f, "selfhost/backend/emit_head.ludic")
|
push(f, "selfhost/backend/emit_head.ludic")
|
||||||
push(f, "selfhost/backend/emit_addr.ludic")
|
push(f, "selfhost/backend/emit_addr.ludic")
|
||||||
|
|
|
||||||
|
|
@ -742,6 +742,9 @@ function cmd_dev_test() -> int {
|
||||||
reject_case("rejected/registry_unknown", "there is no registry Tables", "a def names a registry that exists")
|
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_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")
|
reject_case("rejected/registry_field", "Furnishing has no field colour", "a def's fields are the record's")
|
||||||
|
feat_case("lang/resources", "", "2 Axe 2 Split 10 0 1", "resources.ludic (L9: a registry read from a resource file at compile time, nested records typed by the schema)")
|
||||||
|
reject_case("rejected/resource_field", "bad_tools.lres:3: error: field weight of Tool wants a float", "a resource file's entry is checked at its own line")
|
||||||
|
reject_case("rejected/resource_missing", "cannot read the resource file data/no_such.lres", "a registry's resource file must exist")
|
||||||
|
|
||||||
# EV2 the world table: the mod reflection ABI, callable from Ludic by name.
|
# EV2 the world table: the mod reflection ABI, callable from Ludic by name.
|
||||||
net_case("ecs/world_get", "50 1 7")
|
net_case("ecs/world_get", "50 1 7")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue