attributes: @Tint(SLOT) on a colour field, into the schema - SLOT must be a constant that exists (a row of the program's tint-slot registry); reseeded

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 18:51:27 +03:00
parent 34c2ac2cb9
commit b5bffe99fa
8 changed files with 71123 additions and 70774 deletions

View file

@ -642,6 +642,7 @@ property Tool {
@Color tint: int = 0
@Node(model) grip: string = "" # a node inside the glTF that `model` names
@Clip(model) swing: string = "" # a clip inside it
@Color @Tint(TSLOT_SHELL) shell: int = 0 # a colour for that tint slot
@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
@ -650,11 +651,11 @@ property Tool {
registry Tools of Tool as TL from "data/tools.lres"
```
They change nothing the program does. Three are checked, and every field that fails is reported, not
They change nothing the program does. Four are checked, and every field that fails is reported, not
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
in `_` is a prefix, and otherwise each argument is a constant that must exist; `@Tint(C)` must name a
constant that exists (a row of the program's tint-slot registry); 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
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

View file

@ -8,9 +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).
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`, `@Node(field)`, `@Clip(field)`, `@Derived`, `@Text`, `@Multiline`, `@Key`, and `@AppendOnly` / `@ByKey` on
`@Color`, `@Node(field)`, `@Clip(field)`, `@Tint(SLOT)`, `@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` / `@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
a listing `@OneOf` (`@OneOf(A, B)`, not a prefix `@OneOf(P_)`) naming a constant that does not exist, and `@Tint` naming no constant. 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

@ -24,7 +24,7 @@ program Attributes {
@Range(0, 20.5) @Unit("kg") weight: float = 1.0
@Asset("gltf")
model: string = "" # its model, under assets/kit
@Color tint: int = 16777215
@Color @Tint(VD_STORE) 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

View file

@ -1,6 +1,7 @@
# attributes: @Node / @Clip name a field of its record that names a glTF - `handle` names no field,
# `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)
# @OneOf and @Tint name constants that exist, which KITS_NONE and TSLOT_NONE are not (all six are
# reported)
program R {
property Kit {
key: string = ""
@ -16,6 +17,7 @@ program R {
@Node(kit) hook: string = ""
@Clip(tag) swing: string = ""
@OneOf(KITS_COUNT, KITS_NONE) which: int = 0
@Tint(TSLOT_NONE) shade: int = 0
}
entry { print(KITS_COUNT) }
}

View file

@ -11,15 +11,16 @@
# @Color tint: int = 0
# @Node(model) grip: string = "" # a node inside the glTF the field `model` names
# @Clip(model) idle: string = "" # a clip inside that glTF
# @Color @Tint(TSLOT_HAIR) hair: int = 0 # a colour for that tint slot (a constant that exists)
# @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. Three are checked: @Ref must name a registry that exists,
# They change nothing the program does. Four are checked: @Ref must name a registry that exists,
# @Node and @Clip a field of its record that names a glTF (attrs_node.ludic), and a listing @OneOf
# constants that exist. A
# constants that exist, and @Tint a constant that exists (a slot's row in the game's registry). 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 ...
@ -104,7 +105,7 @@ function at_lead0(raw: pointer) -> pointer {
# the editor attributes a field may carry, and what each takes
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 }
return (a == "Node") or (a == "Clip") or at_flag(a)
return (a == "Node") or (a == "Clip") or (a == "Tint") or at_flag(a)
}
# the attributes that take nothing: they say what a field is
function at_flag(a: pointer) -> bool {
@ -114,7 +115,7 @@ function at_flag(a: pointer) -> bool {
function at_field(g: int, a: pointer) -> void {
let args = at_args()
let n = g_at_argn
if (a == "Ref") or (a == "Node") or (a == "Clip") {
if (a == "Ref") or (a == "Node") or (a == "Clip") or (a == "Tint") {
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)") }
@ -180,6 +181,9 @@ function at_check_field(d: Node, f: Node) -> void {
}
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) }
if (g_at_name[j] == "Tint") and not at_const_exists(g_at_first[j]) {
perr_at(f.file, f.line, f.col, `field {d.s}.{f.s}: @Tint({g_at_first[j]}) names no tint slot - there is no constant {g_at_first[j]}`)
}
j += 1
}
}

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")
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", 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")
diag_json_case("rejected/node_bad", 6, "@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)")
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")