attributes: @Node(field), @Derived, @Text, @Multiline and @Key on a field, into the schema - @Node's field must be @Asset("gltf") or an @Ref to a registry whose record has exactly one, every failure reported; reseeded

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 18:31:17 +03:00
parent b647964839
commit ebfbabdfd8
10 changed files with 73439 additions and 71415 deletions

View file

@ -639,14 +639,19 @@ property Tool {
@Range(0, 20.5) @Unit("kg") weight: float = 1.0
@Asset("gltf") model: string = "" # a file of that kind (any string)
@Color tint: int = 0
@Node(model) grip: string = "" # a node inside the glTF that `model` names
@Derived reach: float = 0.0 # worked out at boot: anything written is overwritten
@Text @Multiline blurb: string = "" # read by the player (so translated), and prose
@Key bind: int = 0 # a key code
}
@AppendOnly @ByKey
registry Tools of Tool as TL from "data/tools.lres"
```
They change nothing the program does. One is checked: `@Ref` must name a registry the program
declares, and a field whose `@Ref` names none is an error naming the field and the registry (every
such field is reported, not only the first).
They change nothing the program does. Two are checked, and every field that fails is reported, not
only the first: `@Ref` must name a registry the program declares, and `@Node(f)` must name a field
`f` of the same record that is either `@Asset("gltf")` or an `@Ref` to a registry whose record has
exactly one `@Asset("gltf")` field (the model is then the referenced row's).
`ludicc app.ludic --emit-schema out.json` (or `ludic schema [file] [-o out.json]`) writes what the
compiler resolved, once the types are checked and every open registry has its entries, as one JSON

View file

@ -8,7 +8,8 @@ prefix, resource file, openness and its entries in their final order after the o
which entries in); every const; and every function a `fn` value can name, with the `fn_type` a field sees (its states stripped).
Deterministic, `"schema_version": 1`. Fields and registries carry editor attributes on the existing
`@` syntax - `@Ref(Registry)`, `@OneOf(PREFIX_)`, `@Range(lo, hi)`, `@Unit("m/s")`, `@Asset("gltf")`,
`@Color`, and `@AppendOnly` / `@ByKey` on a registry - which change nothing but go into the schema;
`@Ref` naming no registry is an error. A field may now carry several attributes. `ludicc --check
`@Color`, `@Node(field)`, `@Derived`, `@Text`, `@Multiline`, `@Key`, and `@AppendOnly` / `@ByKey` on
a registry - which change nothing but go into the schema; `@Ref` naming no registry is an error, and
so is `@Node` naming a field that is not a glTF (`@Asset("gltf")`, or an `@Ref` to one). A field may now carry several attributes. `ludicc --check
--diagnostics=json` (`ludic build --check --diagnostics=json`) prints every error as one JSON array
of `{file, line, col, severity, message}` on stdout; tokens and nodes now know their column.

View file

@ -1,8 +1,8 @@
# attributes.ludic — editor metadata on the @-syntax: which registry a field indexes, which constants
# it takes, its range, its unit, the kind of file it names, that it is a colour, and a registry's own
# rules (append only, saved by key). None of them changes what the program does: `ludicc
# --emit-schema` carries them to an editor with every record, registry entry and const. One is
# checked - @Ref must name a registry that exists.
# --emit-schema` carries them to an editor with every record, registry entry and const. Two are
# checked - @Ref must name a registry that exists, and @Node a field of its record naming a glTF.
#
# Running it prints: 2 1 Lantern 2
program Attributes {
@ -26,9 +26,18 @@ program Attributes {
model: string = "" # its model, under assets/kit
@Color tint: int = 16777215
@OneOf(VD_) stocked_at: int = VD_STORE
@Node(model) grip: string = "grip" # where a hand holds it, a node in that model
@Derived reach: float = 0.0 # worked out at boot from the model
@Text @Multiline blurb: string = "" # what the shop says about it
@Key bind: int = 0 # the key that takes it in hand
}
@AppendOnly
registry Tools of Tool as TL from "data/attr_tools.lres"
# a tool hung on the camp's rack: which, and from which node of ITS model
property RackSlot {
@Ref(Tools) tool: int = 0
@Node(tool) hook: string = "grip"
}
const CAMP_TOOLS: int = 2 * VD_COUNT # how many a camp can carry
# the tool a new camp starts with
function first_tool() -> string { return Tools[0].name }

View file

@ -0,0 +1,18 @@
# attributes: @Node names a field of its record that names a glTF - `handle` names no field, `tag`
# is a plain string, and `kit` refers to a registry whose record has two models (all three are reported)
program R {
property Kit {
key: string = ""
@Asset("gltf") body: string = ""
@Asset("gltf") lod: string = ""
}
registry Kits of Kit
property Tool {
tag: string = ""
@Ref(Kits) kit: int = 0
@Node(handle) grip: string = ""
@Node(tag) tip: string = ""
@Node(kit) hook: string = ""
}
entry { print(KITS_COUNT) }
}

View file

@ -8,10 +8,15 @@
# @Unit("m/s") speed: float = 0.0
# @Asset("gltf") model: string = "" # a file of that kind
# @Color tint: int = 0
# @Node(model) grip: string = "" # a node inside the glTF the field `model` names
# @Derived reach: float = 0.0 # worked out at boot: anything written is overwritten
# @Text @Multiline blurb: string = "" # read by the player, so translated; prose
# @Key bind: int = 0 # a key code
# }
# @AppendOnly @ByKey registry Items of Item as IT from "items.lres"
#
# They change nothing the program does. One is checked: @Ref must name a registry that exists. A
# They change nothing the program does. Two are checked: @Ref must name a registry that exists, and
# @Node a field of its record that names a glTF (attrs_node.ludic). A
# field's or a registry's attributes are a group (Node.at, g_rg_at): a run of names, each with its
# arguments as JSON values - an identifier or a string as a string, a number as written.
var g_at_start: []int = new []int # a group's first attribute ...
@ -92,19 +97,24 @@ function at_lead0(raw: pointer) -> pointer {
}
# the editor attributes a field may carry, and what each takes
function at_field_known(a: pointer) -> bool {
return (a == "Ref") or (a == "OneOf") or (a == "Range") or (a == "Unit") or (a == "Asset") or (a == "Color")
if (a == "Ref") or (a == "OneOf") or (a == "Range") or (a == "Unit") or (a == "Asset") or (a == "Color") { return true }
return (a == "Node") or at_flag(a)
}
# the attributes that take nothing: they say what a field is
function at_flag(a: pointer) -> bool {
return (a == "Color") or (a == "Derived") or (a == "Text") or (a == "Multiline") or (a == "Key")
}
# one field attribute, its '@' and name already read: its arguments, checked for shape, into group g
function at_field(g: int, a: pointer) -> void {
let args = at_args()
let n = g_at_argn
if (a == "Ref") or (a == "OneOf") {
if (a == "Ref") or (a == "OneOf") or (a == "Node") {
if n != 1 or toks[pi - 2].kind != TK_ID { perr(`@{a} takes one name: @{a}(Items)`) }
}
if (a == "Range") and n != 2 { perr("@Range takes the least and the most: @Range(0, 1.5)") }
if (a == "Range") and not g_at_allnum { perr("@Range takes two numbers: @Range(0, 1.5)") }
if ((a == "Unit") or (a == "Asset")) and (n != 1 or toks[pi - 2].kind != TK_STR) { perr(`@{a} takes one string: @{a}("m/s")`) }
if (a == "Color") and n != 0 { perr("@Color takes nothing") }
if at_flag(a) and n != 0 { perr(`@{a} takes nothing`) }
at_push(g, a, args, g_at_arg0, n)
}
# a field's group, made the first time it needs one
@ -135,7 +145,8 @@ function at_registry_take() -> int {
g_at_decl = new []pointer
return g
}
# once every registry is declared: each @Ref names one, or the build stops with every one that does not
# once every registry is declared: each @Ref names one and each @Node a glTF field, or the build
# stops with every one that does not
function at_check_refs() -> void {
var i = 0
while i < len(prog) {
@ -159,6 +170,7 @@ function at_check_field(d: Node, f: Node) -> void {
if (g_at_name[j] == "Ref") and reg_find(g_at_first[j]) < 0 {
perr_at(f.file, f.line, f.col, `field {d.s}.{f.s}: @Ref({g_at_first[j]}) names no registry - there is no registry {g_at_first[j]}`)
}
if g_at_name[j] == "Node" { at_check_node(d, f, g_at_first[j]) }
j += 1
}
}

View file

@ -0,0 +1,69 @@
# attrs_node.ludic — @Node(model): a string naming a node inside the glTF another field of the same
# record names. That field is @Asset("gltf") itself, or an @Ref(R) whose record has exactly one
# @Asset("gltf") field (the model is found through the row). Anything else is an error naming it.
# the field of record d called name, or null
function at_field_of(d: Node, name: pointer) -> Node {
var k = 0
while k < len(d.kids) {
if d.kids[k].s != null and vis_plain(d.kids[k].s) == name { return d.kids[k] }
k += 1
}
return null
}
# field f's argument to attribute a as written, or null when f does not carry it
function at_arg_of(f: Node, a: pointer) -> pointer {
if f.at < 0 { return null }
var j = g_at_start[f.at]
while j < g_at_start[f.at] + g_at_count[f.at] {
if g_at_name[j] == a { return g_at_first[j] }
j += 1
}
return null
}
function at_is_gltf(f: Node) -> bool {
let a = at_arg_of(f, "Asset")
return a != null and a == "gltf"
}
# the record called name (a property or record), or null
function at_record(name: pointer) -> Node {
let plain = vis_plain(name)
var i = 0
while i < len(prog) {
if prog[i].kind == N_COMP and prog[i].s != null and vis_plain(prog[i].s) == plain { return prog[i] }
i += 1
}
return null
}
# how many @Asset("gltf") fields record r carries
function at_gltf_count(r: Node) -> int {
var n = 0
var k = 0
while k < len(r.kids) {
if at_is_gltf(r.kids[k]) { n += 1 }
k += 1
}
return n
}
function at_check_node(d: Node, f: Node, target: pointer) -> void {
let where = `field {d.s}.{f.s}: @Node({target})`
let t = at_field_of(d, target)
if t == null {
perr_at(f.file, f.line, f.col, `{where} names no field - {d.s} has no field {target}`)
return
}
if at_is_gltf(t) { return }
let reg = at_arg_of(t, "Ref")
if reg == null {
perr_at(f.file, f.line, f.col, `{where}: {target} is neither @Asset("gltf") nor an @Ref to a registry of models`)
return
}
let r = reg_find(reg)
if r < 0 { return } # @Ref's own check says so
let rec = at_record(g_rg_type[r])
var n = 0
if rec != null { n = at_gltf_count(rec) }
if n != 1 {
perr_at(f.file, f.line, f.col, `{where}: {target} is an @Ref({reg}), and its record {g_rg_type[r]} has {n} @Asset("gltf") fields, not one`)
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -33,6 +33,7 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/frontend/registry_finish.ludic")
push(f, "selfhost/frontend/registry_open.ludic")
push(f, "selfhost/frontend/attrs.ludic")
push(f, "selfhost/frontend/attrs_node.ludic")
push(f, "selfhost/frontend/resource.ludic")
push(f, "selfhost/frontend/defaults.ludic")
push(f, "selfhost/frontend/view.ludic")

View file

@ -1200,6 +1200,7 @@ function cmd_dev_test() -> int {
reject_count("rejected/every_error", 3, "every type error in a program is reported at once, not only the first")
diag_json_case("rejected/every_error", 3, "\"line\": 8, \"col\": 15, \"severity\": \"error\", \"message\": \"half takes 1 argument(s) and this call gives 2\"", "--diagnostics=json: every type error as {file, line, col, severity, message}")
diag_json_case("rejected/ref_unknown", 2, "@Ref(Shops) names no registry", "--diagnostics=json: every @Ref that names no registry")
diag_json_case("rejected/node_bad", 3, "its record Kit has 2 @Asset(", "--diagnostics=json: every @Node that names no model field")
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")
@ -1232,9 +1233,11 @@ function cmd_dev_test() -> int {
reject_case("rejected/registry_unknown", "there is no registry Tables", "a def names a registry that exists")
feat_case("lang/attributes", "", "2 1 Lantern 2", "attributes.ludic (editor attributes: @Ref, @OneOf, @Range, @Unit, @Asset, @Color, @AppendOnly, @ByKey - no effect on the program)")
reject_case("rejected/ref_unknown", "field Tool.seller: @Ref(Vendorz) names no registry", "@Ref names a registry that exists")
reject_case("rejected/node_bad", "field Tool.grip: @Node(handle) names no field", "@Node names a field of its record")
schema_case("lang/attributes", "{\"name\": \"weight\", \"type\": \"float\", \"default\": \"1.0\", \"file\": \"examples/lang/attributes.ludic\", \"line\": 24, \"col\": 33, \"attributes\": [{\"name\": \"Range\", \"args\": [0, 20.5]}, {\"name\": \"Unit\", \"args\": [\"kg\"]}]", "--emit-schema: a field's type, default, place and attributes")
schema_case("lang/attributes", "{\"key\": \"lantern\", \"constant\": \"TL_LANTERN\", \"index\": 1, \"file\": \"examples/lang/data/attr_tools.lres\", \"line\": 3, \"col\": 1", "--emit-schema: a resource entry's constant, index and place")
schema_case("lang/attributes", "\"fn_type\": \"fn(Tool,int)->float\", \"params\": [\"Tool\", \"int\"]", "--emit-schema: a function with arguments, its fn_type spelled as a field's")
schema_case("lang/attributes", "{\"name\": \"Node\", \"args\": [\"tool\"]}", "--emit-schema: @Node and the field it reads its model from")
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")
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)")