attributes: @Material(field) resolved as @Node is; @OneOf on a string field takes words, and every registry row's value is checked against them - words on another field, or constants on a string, is an error; reseeded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d82dc3162f
commit
81cb992fad
10 changed files with 75030 additions and 73286 deletions
17
LANGUAGE.md
17
LANGUAGE.md
|
|
@ -642,6 +642,8 @@ 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
|
||||
@Material(model) finish: string = "" # a material inside it
|
||||
@OneOf("box", "hull") shape: string = "" # a string field: one of these words
|
||||
@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
|
||||
|
|
@ -651,12 +653,15 @@ property Tool {
|
|||
registry Tools of Tool as TL from "data/tools.lres"
|
||||
```
|
||||
|
||||
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; `@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).
|
||||
They change nothing the program does. A target that no part of the program declares - the registry
|
||||
an `@Ref` names, the constant of an `@Tint` or a listed `@OneOf` - is a warning, and the schema marks
|
||||
the attribute `"unresolved": true`: a package can name the game's registry without importing it.
|
||||
Everything else is an error, every one reported: a target that exists but is another kind
|
||||
(`@Ref(Vendor)` on a record); `@OneOf` of the wrong kind for its field - on a string field the
|
||||
arguments are words (`@OneOf("box", "hull")`) and every registry row's value must be one of them, on
|
||||
any other field a prefix ending in `_` or constants; and `@Node(f)`, `@Clip(f)` or `@Material(f)`
|
||||
naming anything but a field `f` of the same record that is `@Asset("gltf")`, or an `@Ref` to a
|
||||
registry whose record has exactly one `@Asset("gltf")` field (the model is then the 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
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ 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)`, `@Tint(SLOT)`, `@Derived`, `@Text`, `@Multiline`, `@Key`, and `@AppendOnly` / `@ByKey` on
|
||||
`@Color`, `@Node(field)`, `@Clip(field)`, `@Material(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, and `@Tint` naming no constant. A field may now carry several attributes. `ludicc --check
|
||||
|
|
@ -17,3 +17,8 @@ of `{file, line, col, severity, message}` on stdout; tokens and nodes now know t
|
|||
|
||||
`Build.schema_hash()` answers FNV-1a 64 of the program's own schema (the bytes `ludic schema` prints),
|
||||
computed only when a program names it, and 0 under `ludicc --release`, which `ludic bundle` now passes.
|
||||
|
||||
A target no part of the program declares (an `@Ref` registry, an `@Tint` or listed `@OneOf` constant) is
|
||||
a warning and `"unresolved": true` in the schema, so a package can name the game's registry; a name of
|
||||
another kind is an error. `@OneOf` on a string field takes words, and every registry row's value is
|
||||
checked against them.
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ program Attributes {
|
|||
@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
|
||||
@Material(model) finish: string = "steel" # a material in that model
|
||||
@OneOf("box", "hull") shape: string = "box" # what it collides as: one of these words, row by row
|
||||
}
|
||||
@AppendOnly
|
||||
registry Tools of Tool as TL from "data/attr_tools.lres"
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@ lantern {
|
|||
seller: VD_MARINA
|
||||
weight: 0.75
|
||||
stocked_at: VD_MARINA
|
||||
shape: "hull"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# attributes: @Node / @Clip name a field of their 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: four errors.
|
||||
# @OneOf's KITS_NONE and @Tint's TSLOT_NONE are in no part of the program: two warnings.
|
||||
# `tag` is a plain string, and `kit` refers to a registry whose record has two models.
|
||||
# @Material(tag) is the same fault; words on an int, constants on a string, and a row whose shape is
|
||||
# no word of its @OneOf are the rest: eight errors. @OneOf's KITS_NONE and @Tint's TSLOT_NONE are in
|
||||
# no part of the program: two warnings.
|
||||
program R {
|
||||
property Kit {
|
||||
key: string = ""
|
||||
|
|
@ -17,6 +19,12 @@ program R {
|
|||
@Clip(tag) swing: string = ""
|
||||
@OneOf(KITS_COUNT, KITS_NONE) which: int = 0
|
||||
@Tint(TSLOT_NONE) shade: int = 0
|
||||
@Material(tag) paint: string = ""
|
||||
@OneOf("a", "b") size: int = 0
|
||||
@OneOf(KITS_COUNT) form: string = ""
|
||||
@OneOf("box", "hull") shape: string = "box"
|
||||
}
|
||||
registry Tools of Tool
|
||||
def Tools crate { shape: "cube" }
|
||||
entry { print(KITS_COUNT) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
# @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
|
||||
# @Material(model) skin: string = "" # a material inside it
|
||||
# @OneOf("box", "hull") shape: string = "" # a string field: one of these words
|
||||
# @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
|
||||
|
|
@ -19,9 +21,10 @@
|
|||
# @AppendOnly @ByKey registry Items of Item as IT from "items.lres"
|
||||
#
|
||||
# They change nothing the program does. A target the program does not have at all is a warning and
|
||||
# "unresolved" in the schema (a package names the game's registry it cannot import). 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, and @Tint a constant that exists (a slot's row in the game's registry). A
|
||||
# "unresolved" in the schema (a package names the game's registry it cannot import). Checked: @Ref
|
||||
# names a registry; @Node, @Clip and @Material a field of their record naming a glTF
|
||||
# (attrs_node.ludic); a listing @OneOf constants (or, on a string field, words - and every row's value
|
||||
# one of them); and @Tint a constant (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 ...
|
||||
|
|
@ -47,6 +50,8 @@ function at_push(g: int, name: pointer, args: pointer, first: pointer, nargs: in
|
|||
push(g_at_first, first)
|
||||
push(g_at_nargs, nargs)
|
||||
push(g_at_unres, false)
|
||||
push(g_at_strs, g_at_strs_next)
|
||||
g_at_strs_next = false
|
||||
g_at_count[g] = g_at_count[g] + 1
|
||||
}
|
||||
# `(a, "b", -1.5)` after an attribute's name, if it is there, as a JSON array; the first argument
|
||||
|
|
@ -55,11 +60,15 @@ var g_at_arg0: pointer = null
|
|||
var g_at_argn: int = 0
|
||||
var g_at_allnum: bool = true # every argument a number
|
||||
var g_at_allid: bool = true # every argument a name
|
||||
var g_at_allstr: bool = true # every argument a string
|
||||
var g_at_strs: []bool = new []bool # per attribute: its arguments were all strings
|
||||
var g_at_strs_next: bool = false # what at_push files for the attribute it pushes
|
||||
function at_args() -> pointer {
|
||||
g_at_arg0 = null
|
||||
g_at_argn = 0
|
||||
g_at_allnum = true
|
||||
g_at_allid = true
|
||||
g_at_allstr = true
|
||||
if not is_op("(") { return "[]" }
|
||||
pi += 1
|
||||
var out: pointer = "["
|
||||
|
|
@ -69,6 +78,7 @@ function at_args() -> pointer {
|
|||
if is_op("-") { neg = true; pi += 1 }
|
||||
let t = toks[pi]
|
||||
if t.kind != TK_ID { g_at_allid = false }
|
||||
if t.kind != TK_STR { g_at_allstr = false }
|
||||
var v: pointer = null
|
||||
var raw: pointer = null
|
||||
if t.kind == TK_INT {
|
||||
|
|
@ -108,7 +118,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 (a == "Tint") or at_flag(a)
|
||||
return (a == "Node") or (a == "Clip") or (a == "Material") or (a == "Tint") or at_flag(a)
|
||||
}
|
||||
# the attributes that take nothing: they say what a field is
|
||||
function at_flag(a: pointer) -> bool {
|
||||
|
|
@ -118,10 +128,11 @@ 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") or (a == "Tint") {
|
||||
if (a == "Ref") or (a == "Node") or (a == "Clip") or (a == "Material") 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)") }
|
||||
if (a == "OneOf") and (n < 1 or not (g_at_allid or g_at_allstr)) { perr("@OneOf takes a prefix, constants or words: @OneOf(GR_), @OneOf(GR_GOLD, GR_SILVER) or @OneOf(\"box\", \"hull\")") }
|
||||
g_at_strs_next = (a == "OneOf") and g_at_allstr
|
||||
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")`) }
|
||||
|
|
@ -180,7 +191,7 @@ function at_check_field(d: Node, f: Node) -> void {
|
|||
var j = g_at_start[g]
|
||||
while j < g_at_start[g] + g_at_count[g] {
|
||||
if (g_at_name[j] == "Ref") and reg_find(g_at_first[j]) < 0 { at_target(d, f, j, "a registry") }
|
||||
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] == "Node") or (g_at_name[j] == "Clip") or (g_at_name[j] == "Material") { 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]) { at_const_target(d, f, j, g_at_first[j]) }
|
||||
j += 1
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
# attrs_node.ludic — @Node(model) / @Clip(model): a string naming a node or a clip inside the glTF
|
||||
# attrs_node.ludic — @Node(model) / @Clip(model) / @Material(model): a string naming a node, a clip or a
|
||||
# material 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.
|
||||
|
||||
|
|
@ -68,17 +69,71 @@ function at_check_node(d: Node, f: Node, a: pointer, target: pointer) -> void {
|
|||
}
|
||||
}
|
||||
|
||||
# @OneOf: one argument ending in _ is a prefix; otherwise every argument is a constant that must exist
|
||||
# @OneOf: on a string field, words, and every row's value one of them; otherwise one argument ending
|
||||
# in _ is a prefix, or every argument a constant that must exist
|
||||
function at_check_oneof(d: Node, f: Node, j: int) -> void {
|
||||
let where = `field {d.s}.{f.s}: @OneOf`
|
||||
let words = g_at_strs[j]
|
||||
let is_text = f.ty != null and f.ty == "string"
|
||||
if words and not is_text {
|
||||
perr_at(f.file, f.line, f.col, `{where} lists words, and {f.s} is not a string - name constants instead`)
|
||||
return
|
||||
}
|
||||
if is_text and not words {
|
||||
perr_at(f.file, f.line, f.col, `{where} names constants, and {f.s} is a string - list the words it may hold: @OneOf("a", "b")`)
|
||||
return
|
||||
}
|
||||
let names = at_arg_names(g_at_args[j])
|
||||
if words {
|
||||
at_check_rows(d, f, names)
|
||||
return
|
||||
}
|
||||
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]) { at_const_target(d, f, j, names[i]) }
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
# every registry row of record d that writes field f as a string: one of the words
|
||||
function at_check_rows(d: Node, f: Node, words: []pointer) -> void {
|
||||
let plain = vis_plain(d.s)
|
||||
var i = 0
|
||||
while i < len(g_df_rec) {
|
||||
let r = reg_find(g_df_reg[i])
|
||||
if r >= 0 and vis_plain(g_rg_type[r]) == plain { at_check_row(d, f, g_df_rec[i], g_df_key[i], words) }
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
function at_check_row(d: Node, f: Node, rec: Node, key: pointer, words: []pointer) -> void {
|
||||
var k = 0
|
||||
while k < len(rec.kids) {
|
||||
let fi = rec.kids[k]
|
||||
if fi.kind == E_FINIT and fi.s == f.s and fi.a != null and fi.a.kind == E_STR and not at_word_in(fi.a.s, words) {
|
||||
perr_at(fi.file, fi.line, fi.col, `{key}.{f.s} is "{fi.a.s}", and field {d.s}.{f.s} is @OneOf({at_words_text(words)})`)
|
||||
}
|
||||
k += 1
|
||||
}
|
||||
}
|
||||
function at_word_in(w: pointer, words: []pointer) -> bool {
|
||||
var i = 0
|
||||
while i < len(words) {
|
||||
if words[i] == w { return true }
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
function at_words_text(words: []pointer) -> pointer {
|
||||
var out: pointer = ""
|
||||
var i = 0
|
||||
while i < len(words) {
|
||||
if i > 0 { out = out + ", " }
|
||||
out = out + "\"" + words[i] + "\""
|
||||
i += 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
# 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
|
||||
|
|
|
|||
72221
selfhost/ludicc.seed.ll
72221
selfhost/ludicc.seed.ll
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -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): this program has no Shops - left unresolved", "--diagnostics=json: an @Ref to a registry the program lacks is a warning beside the error")
|
||||
diag_json_case("rejected/node_bad", 6, "this program has no constant KITS_NONE - left unresolved", "--diagnostics=json: every @Node / @Clip that names no model field, and every @OneOf constant that does not exist")
|
||||
diag_json_case("rejected/node_bad", 10, "crate.shape is \\\"cube\\\", and field Tool.shape is @OneOf(", "--diagnostics=json: every @Node / @Clip / @Material with no model field, @OneOf of the wrong kind or a row outside its words, and the targets the program lacks as warnings")
|
||||
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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue