attributes: @Clip(field) resolved as @Node is, and @OneOf takes constants as well as a prefix - one argument ending in _ is a prefix, otherwise each is a constant that must exist; reseeded

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 18:34:33 +03:00
parent ebfbabdfd8
commit 34c2ac2cb9
9 changed files with 73220 additions and 71912 deletions

View file

@ -636,10 +636,12 @@ the lines above it:
property Tool { property Tool {
@Ref(Vendors) seller: int = 0 # an index into that registry: its entries are offered @Ref(Vendors) seller: int = 0 # an index into that registry: its entries are offered
@OneOf(GR_) grade: int = 0 # one of the constants whose names start GR_ @OneOf(GR_) grade: int = 0 # one of the constants whose names start GR_
@OneOf(GR_GOLD, GR_SILVER) medal: int = 0 # or one of these constants
@Range(0, 20.5) @Unit("kg") weight: float = 1.0 @Range(0, 20.5) @Unit("kg") weight: float = 1.0
@Asset("gltf") model: string = "" # a file of that kind (any string) @Asset("gltf") model: string = "" # a file of that kind (any string)
@Color tint: int = 0 @Color tint: int = 0
@Node(model) grip: string = "" # a node inside the glTF that `model` names @Node(model) grip: string = "" # a node inside the glTF that `model` names
@Clip(model) swing: string = "" # a clip inside it
@Derived reach: float = 0.0 # worked out at boot: anything written is overwritten @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 @Text @Multiline blurb: string = "" # read by the player (so translated), and prose
@Key bind: int = 0 # a key code @Key bind: int = 0 # a key code
@ -648,8 +650,10 @@ property Tool {
registry Tools of Tool as TL from "data/tools.lres" registry Tools of Tool as TL from "data/tools.lres"
``` ```
They change nothing the program does. Two are checked, and every field that fails is reported, not They change nothing the program does. Three 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 only the first: `@Ref` must name a registry the program declares; `@OneOf` with one argument ending
in `_` is a prefix, and otherwise each argument is a constant that must exist; and `@Node(f)` or
`@Clip(f)` must name a field
`f` of the same record that is either `@Asset("gltf")` or an `@Ref` to a registry whose record has `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). exactly one `@Asset("gltf")` field (the model is then the referenced row's).

View file

@ -8,8 +8,9 @@ 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). 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 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")`, `@` syntax - `@Ref(Registry)`, `@OneOf(PREFIX_)`, `@Range(lo, hi)`, `@Unit("m/s")`, `@Asset("gltf")`,
`@Color`, `@Node(field)`, `@Derived`, `@Text`, `@Multiline`, `@Key`, and `@AppendOnly` / `@ByKey` on `@Color`, `@Node(field)`, `@Clip(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 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 so is `@Node` / `@Clip` naming a field that is not a glTF (`@Asset("gltf")`, or an `@Ref` to one), and
a listing `@OneOf` (`@OneOf(A, B)`, not a prefix `@OneOf(P_)`) naming a constant that does not exist. A field may now carry several attributes. `ludicc --check
--diagnostics=json` (`ludic build --check --diagnostics=json`) prints every error as one JSON array --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. of `{file, line, col, severity, message}` on stdout; tokens and nodes now know their column.

View file

@ -30,6 +30,8 @@ program Attributes {
@Derived reach: float = 0.0 # worked out at boot from the model @Derived reach: float = 0.0 # worked out at boot from the model
@Text @Multiline blurb: string = "" # what the shop says about it @Text @Multiline blurb: string = "" # what the shop says about it
@Key bind: int = 0 # the key that takes it in hand @Key bind: int = 0 # the key that takes it in hand
@Clip(model) swing: string = "swing" # the clip it plays in use
@OneOf(VD_STORE, VD_MARINA) sold_at: int = VD_MARINA
} }
@AppendOnly @AppendOnly
registry Tools of Tool as TL from "data/attr_tools.lres" registry Tools of Tool as TL from "data/attr_tools.lres"

View file

@ -1,5 +1,6 @@
# attributes: @Node names a field of its record that names a glTF - `handle` names no field, `tag` # attributes: @Node / @Clip name a field of its record that names a glTF - `handle` names no field,
# is a plain string, and `kit` refers to a registry whose record has two models (all three are reported) # `tag` is a plain string, `kit` refers to a registry whose record has two models - and a listing
# @OneOf names constants that exist, which KITS_NONE is not (all five are reported)
program R { program R {
property Kit { property Kit {
key: string = "" key: string = ""
@ -13,6 +14,8 @@ program R {
@Node(handle) grip: string = "" @Node(handle) grip: string = ""
@Node(tag) tip: string = "" @Node(tag) tip: string = ""
@Node(kit) hook: string = "" @Node(kit) hook: string = ""
@Clip(tag) swing: string = ""
@OneOf(KITS_COUNT, KITS_NONE) which: int = 0
} }
entry { print(KITS_COUNT) } entry { print(KITS_COUNT) }
} }

View file

@ -3,20 +3,23 @@
# #
# property Item { # property Item {
# @Ref(Vendors) seller: int = 0 # an index into that registry (its entries are offered) # @Ref(Vendors) seller: int = 0 # an index into that registry (its entries are offered)
# @OneOf(GR_) grade: int = 0 # one of the constants that start GR_ # @OneOf(GR_) grade: int = 0 # one of the constants that start GR_ ...
# @OneOf(WILD_MEADOW, WILD_SHORE) hab: int = 0 # ... or one of these (each must exist)
# @Range(0, 1.5) weight: float = 0.4 # @Range(0, 1.5) weight: float = 0.4
# @Unit("m/s") speed: float = 0.0 # @Unit("m/s") speed: float = 0.0
# @Asset("gltf") model: string = "" # a file of that kind # @Asset("gltf") model: string = "" # a file of that kind
# @Color tint: int = 0 # @Color tint: int = 0
# @Node(model) grip: string = "" # a node inside the glTF the field `model` names # @Node(model) grip: string = "" # a node inside the glTF the field `model` names
# @Clip(model) idle: string = "" # a clip inside that glTF
# @Derived reach: float = 0.0 # worked out at boot: anything written is overwritten # @Derived reach: float = 0.0 # worked out at boot: anything written is overwritten
# @Text @Multiline blurb: string = "" # read by the player, so translated; prose # @Text @Multiline blurb: string = "" # read by the player, so translated; prose
# @Key bind: int = 0 # a key code # @Key bind: int = 0 # a key code
# } # }
# @AppendOnly @ByKey registry Items of Item as IT from "items.lres" # @AppendOnly @ByKey registry Items of Item as IT from "items.lres"
# #
# They change nothing the program does. Two are checked: @Ref must name a registry that exists, and # They change nothing the program does. Three are checked: @Ref must name a registry that exists,
# @Node a field of its record that names a glTF (attrs_node.ludic). A # @Node and @Clip a field of its record that names a glTF (attrs_node.ludic), and a listing @OneOf
# constants that exist. A
# field's or a registry's attributes are a group (Node.at, g_rg_at): a run of names, each with its # 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. # 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 ... var g_at_start: []int = new []int # a group's first attribute ...
@ -47,10 +50,12 @@ function at_push(g: int, name: pointer, args: pointer, first: pointer, nargs: in
var g_at_arg0: pointer = null var g_at_arg0: pointer = null
var g_at_argn: int = 0 var g_at_argn: int = 0
var g_at_allnum: bool = true # every argument a number var g_at_allnum: bool = true # every argument a number
var g_at_allid: bool = true # every argument a name
function at_args() -> pointer { function at_args() -> pointer {
g_at_arg0 = null g_at_arg0 = null
g_at_argn = 0 g_at_argn = 0
g_at_allnum = true g_at_allnum = true
g_at_allid = true
if not is_op("(") { return "[]" } if not is_op("(") { return "[]" }
pi += 1 pi += 1
var out: pointer = "[" var out: pointer = "["
@ -59,6 +64,7 @@ function at_args() -> pointer {
var neg = false var neg = false
if is_op("-") { neg = true; pi += 1 } if is_op("-") { neg = true; pi += 1 }
let t = toks[pi] let t = toks[pi]
if t.kind != TK_ID { g_at_allid = false }
var v: pointer = null var v: pointer = null
var raw: pointer = null var raw: pointer = null
if t.kind == TK_INT { if t.kind == TK_INT {
@ -98,7 +104,7 @@ function at_lead0(raw: pointer) -> pointer {
# the editor attributes a field may carry, and what each takes # the editor attributes a field may carry, and what each takes
function at_field_known(a: pointer) -> bool { function at_field_known(a: pointer) -> bool {
if (a == "Ref") or (a == "OneOf") or (a == "Range") or (a == "Unit") or (a == "Asset") or (a == "Color") { return true } 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) return (a == "Node") or (a == "Clip") or at_flag(a)
} }
# the attributes that take nothing: they say what a field is # the attributes that take nothing: they say what a field is
function at_flag(a: pointer) -> bool { function at_flag(a: pointer) -> bool {
@ -108,9 +114,10 @@ function at_flag(a: pointer) -> bool {
function at_field(g: int, a: pointer) -> void { function at_field(g: int, a: pointer) -> void {
let args = at_args() let args = at_args()
let n = g_at_argn let n = g_at_argn
if (a == "Ref") or (a == "OneOf") or (a == "Node") { if (a == "Ref") or (a == "Node") or (a == "Clip") {
if n != 1 or toks[pi - 2].kind != TK_ID { perr(`@{a} takes one name: @{a}(Items)`) } if n != 1 or toks[pi - 2].kind != TK_ID { perr(`@{a} takes one name: @{a}(Items)`) }
} }
if (a == "OneOf") and (n < 1 or not g_at_allid) { perr("@OneOf takes a prefix or constants: @OneOf(GR_) or @OneOf(GR_GOLD, GR_SILVER)") }
if (a == "Range") and n != 2 { perr("@Range takes the least and the most: @Range(0, 1.5)") } 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 == "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 == "Unit") or (a == "Asset")) and (n != 1 or toks[pi - 2].kind != TK_STR) { perr(`@{a} takes one string: @{a}("m/s")`) }
@ -145,7 +152,8 @@ function at_registry_take() -> int {
g_at_decl = new []pointer g_at_decl = new []pointer
return g return g
} }
# once every registry is declared: each @Ref names one and each @Node a glTF field, or the build # once every registry is declared: each @Ref names one, each @Node / @Clip a glTF field and a
# listing @OneOf constants that exist, or the build
# stops with every one that does not # stops with every one that does not
function at_check_refs() -> void { function at_check_refs() -> void {
var i = 0 var i = 0
@ -170,7 +178,8 @@ function at_check_field(d: Node, f: Node) -> void {
if (g_at_name[j] == "Ref") and reg_find(g_at_first[j]) < 0 { 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]}`) 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]) } if (g_at_name[j] == "Node") or (g_at_name[j] == "Clip") { at_check_node(d, f, g_at_name[j], g_at_first[j]) }
if g_at_name[j] == "OneOf" { at_check_oneof(d, f, j) }
j += 1 j += 1
} }
} }

View file

@ -1,5 +1,5 @@
# attrs_node.ludic — @Node(model): a string naming a node inside the glTF another field of the same # attrs_node.ludic — @Node(model) / @Clip(model): a string naming a node or a clip inside the glTF
# record names. That field is @Asset("gltf") itself, or an @Ref(R) whose record has exactly one # 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. # @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 # the field of record d called name, or null
@ -45,8 +45,8 @@ function at_gltf_count(r: Node) -> int {
} }
return n return n
} }
function at_check_node(d: Node, f: Node, target: pointer) -> void { function at_check_node(d: Node, f: Node, a: pointer, target: pointer) -> void {
let where = `field {d.s}.{f.s}: @Node({target})` let where = `field {d.s}.{f.s}: @{a}({target})`
let t = at_field_of(d, target) let t = at_field_of(d, target)
if t == null { if t == null {
perr_at(f.file, f.line, f.col, `{where} names no field - {d.s} has no field {target}`) perr_at(f.file, f.line, f.col, `{where} names no field - {d.s} has no field {target}`)
@ -67,3 +67,41 @@ function at_check_node(d: Node, f: Node, target: pointer) -> void {
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`) 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`)
} }
} }
# @OneOf: one argument ending in _ is a prefix; otherwise every argument is a constant that must exist
function at_check_oneof(d: Node, f: Node, j: int) -> void {
let first = g_at_first[j]
if g_at_nargs[j] == 1 and first[len(first) - 1] == '_' { return }
let names = at_arg_names(g_at_args[j])
var i = 0
while i < len(names) {
if not at_const_exists(names[i]) {
perr_at(f.file, f.line, f.col, `field {d.s}.{f.s}: @OneOf names {names[i]}, and there is no constant {names[i]}`)
}
i += 1
}
}
# the names in an attribute's JSON arguments (["A", "B"]): each a quoted identifier
function at_arg_names(args: pointer) -> []pointer {
let out = new []pointer
var i = 0
var at = -1
while i < len(args) {
if args[i] == '"' {
if at < 0 { at = i + 1 } else {
push(out, args[at .. i])
at = -1
}
}
i += 1
}
return out
}
function at_const_exists(name: pointer) -> bool {
var i = 0
while i < len(prog) {
if prog[i].kind == N_CONST and prog[i].s != null and vis_plain(prog[i].s) == name { return true }
i += 1
}
return false
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1200,7 +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") 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/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/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") diag_json_case("rejected/node_bad", 5, "@OneOf names KITS_NONE, and there is no constant KITS_NONE", "--diagnostics=json: every @Node / @Clip that names no model field, and every @OneOf constant that does not exist")
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)") 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_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") reject_case("rejected/generic_mismatch", "words wants a Pool<string> and this is a Pool<int>", "two instances of one generic are two types")