feat(lang): def REGISTRY from "file.lres" - a game fills an open registry from its own file

The entries are read and checked against the registry's record as a
registry ... from file is, errors at the resource file's line, and they
are defs of the module that wrote the line: the registry must be open to
it, and they take that module's place in the stable order. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 07:22:05 +03:00
parent a09b622a1d
commit a5f52c4581
13 changed files with 43732 additions and 43470 deletions

View file

@ -400,6 +400,22 @@ The entries are compiled in: nothing is parsed at start-up, and a build that suc
every resource it uses. The path is the project's (where the build runs), else beside the file that every resource it uses. The path is the project's (where the build runs), else beside the file that
declares the registry. declares the registry.
A module that extends an open registry can bring its entries from a resource file of its own:
```ludic
# doc-check: skip — the file it names is beside the example
import "crafting" # module crafting: export open registry Recipes of Recipe as RC
def Recipes from "recipes.lres" # the game's recipes, after crafting's own
```
`def REGISTRY from "file.lres"` reads the file as `registry ... from` does - the project's path, else
beside the file that says it - and checks every entry against the registry's record, an error
naming the resource file's own line (`bad_recipes.lres:3: error: field minutes of Recipe wants an
int and this is a string`). Its entries are defs of the module that wrote the line, so the registry
must be open (and exported) to it, and they take that module's place in the order: the declaring
module's entries, then each other module's by module name, and within a file, file order.
### Default parameters, and calls that name what they change ### Default parameters, and calls that name what they change
A parameter can have a default, and a call leaves out what it does not change - the last ones when A parameter can have a default, and a call leaves out what it does not change - the last ones when

View file

@ -0,0 +1,7 @@
bump: minor
type: feature
**`def Recipes from "recipes.lres"` - a game fills a package's open registry from its own resource
file.** The entries are checked against the registry's record as the file is read, with errors at
the resource file's line, and they are defs of the module that wrote the line: the registry must be
open to it, and they sit in the stable order (the declaring module's entries, then other modules'
by name, and file order within a file).

View file

@ -0,0 +1,9 @@
# crafting/index.ludic - a registry the game may add to, and the package's own first entry
module crafting
export property Recipe {
key: string = ""
name: string = ""
minutes: int = 0
}
export open registry Recipes of Recipe as RC
def Recipes rope { name: "Rope", minutes: 10 }

View file

@ -0,0 +1,3 @@
# recipes.lres - the game's own recipes, added to crafting's open registry
tea { name: "Pine tea", minutes: 5 }
splint { name: "Splint", minutes: 20 }

View file

@ -0,0 +1,12 @@
# registry_extend.ludic — L8 + L9: a game fills a package's open registry from its own resource
# file with `def Recipes from "recipes.lres"`. The entries are checked against Recipe as the file
# is read, and they come after the package's own, in file order.
#
# Running it prints: 3 Rope Pine tea Splint 20
import "crafting"
program RegistryExtend {
def Recipes from "recipes.lres"
entry {
print(`{RC_COUNT} {Recipes[0].name} {Recipes[RC_TEA].name} {Recipes[2].name} {Recipes[recipes_find("splint")].minutes}`)
}
}

View file

@ -0,0 +1,3 @@
# bad_recipes.lres - the second entry gives minutes as text
tea { name: "Pine tea", minutes: 5 }
splint { name: "Splint", minutes: "twenty" }

View file

@ -0,0 +1,8 @@
# craft_kit/index.ludic - an open registry of recipes
module crafting
export property Recipe {
key: string = ""
name: string = ""
minutes: int = 0
}
export open registry Recipes of Recipe as RC

View file

@ -0,0 +1,8 @@
# L8 + L9: a resource file that extends a registry is checked against its record, at its own line
import "craft_kit"
program RegistryExtendField {
def Recipes from "bad_recipes.lres"
entry {
print(RC_COUNT)
}
}

View file

@ -75,9 +75,16 @@ function parse_registry() -> void {
if from != null { res_read(v, from) } if from != null { res_read(v, from) }
} }
# def REGISTRY key { field: value, ... } # def REGISTRY key { field: value, ... }
# def REGISTRY from "file.lres" - every entry of a resource file, as defs of this file's module
function parse_def() -> void { function parse_def() -> void {
pi += 1 pi += 1
let reg = eat_id() let reg = eat_id()
if is_id("from") and toks[pi + 1].kind == TK_STR {
let from = toks[pi + 1].text
pi += 2
res_read_into(reg, from, `def {reg} from`)
return
}
let key = eat_id() let key = eat_id()
skipnl() skipnl()
let rec = record() let rec = record()

View file

@ -16,9 +16,14 @@ function res_path(from: pointer, declaring: pointer) -> pointer {
return join_path(dir_of(declaring), from) return join_path(dir_of(declaring), from)
} }
function res_read(v: Node, from: pointer) -> void { function res_read(v: Node, from: pointer) -> void {
res_read_into(v.s, from, `registry {v.s}`)
}
# the entries of a resource file into registry `reg`, as defs of the reading file's module: a
# registry's own file, or `def REG from "file.lres"` in a module that extends an open registry
function res_read_into(reg: pointer, from: pointer, who: pointer) -> void {
let path = res_path(from, g_parse_file) let path = res_path(from, g_parse_file)
let src = read_file(path) let src = read_file(path)
if src == null { perr(`registry {v.s}: cannot read the resource file {from}`) } if src == null { perr(`{who}: cannot read the resource file {from}`) }
let saved_toks = toks let saved_toks = toks
let saved_pi = pi let saved_pi = pi
let saved_file = g_parse_file let saved_file = g_parse_file
@ -36,7 +41,7 @@ function res_read(v: Node, from: pointer) -> void {
let key = eat_id() let key = eat_id()
skipnl() skipnl()
let rec = record() let rec = record()
push(g_df_reg, v.s) push(g_df_reg, reg)
push(g_df_key, key) push(g_df_key, key)
push(g_df_rec, rec) push(g_df_rec, rec)
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -820,6 +820,8 @@ function cmd_dev_test() -> int {
feat_case("modules/open_registry", "", "clock animals weather / 2 2 3", "open_registry.ludic (L8+L3: an open registry takes other modules' defs; its own entries first, then by module name)") feat_case("modules/open_registry", "", "clock animals weather / 2 2 3", "open_registry.ludic (L8+L3: an open registry takes other modules' defs; its own entries first, then by module name)")
reject_case("rejected/registry_closed", "registry Tools is not open to other modules", "a def from another module into a registry that is not open is refused") reject_case("rejected/registry_closed", "registry Tools is not open to other modules", "a def from another module into a registry that is not open is refused")
reject_case("rejected/registry_hidden", "Tools is private to module kit", "a def goes through visibility: the registry must be exported") reject_case("rejected/registry_hidden", "Tools is private to module kit", "a def goes through visibility: the registry must be exported")
feat_case("modules/registry_extend", "", "3 Rope Pine tea Splint 20", "registry_extend.ludic (L8+L9: `def Recipes from \"recipes.lres\"` fills a package's open registry from the game's file)")
reject_case("rejected/registry_extend_field", "bad_recipes.lres:3: error: field minutes of Recipe wants an int", "a resource file extending a registry is checked at its own line")
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)") 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_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") reject_case("rejected/resource_missing", "cannot read the resource file data/no_such.lres", "a registry's resource file must exist")