tests: the map-scoped tables' example and rejected fixtures

examples/lang/permap (two maps, a chunked table with a negative, a positive and a
missing chunk, constants and fn by name, a nested list, a cross-row @Ref, a reload
shrinking in place, a bad file's file:line:col; built under arena strict with an @On
frame root), --check fixtures for a wrong field, a wrong type, an unknown constant and
a dangling cross-row @Ref, @Ref(PerMap) on an int, as PREFIX, and a @Unit warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 21:03:02 +03:00
parent 2f4860ae71
commit 5a751434ac
17 changed files with 181 additions and 0 deletions

View file

@ -0,0 +1,3 @@
# the chunk west of the origin: {cx} is -1
t1 { kind: MDL_TENT, x: -10.0, z: 3.0 }
t2 { kind: 2, x: -20.5 }

View file

@ -0,0 +1 @@
r1 { kind: 1, x: 3.0, z: 130.0 }

View file

@ -0,0 +1,6 @@
# alpha's props: a constant by name, a nested list of records, a list of strings, a nested
# record, a fn by name, and a cross-row @Ref (home names a row of alpha's spots.lres)
tent { model: MDL_TENT, yaw: 90.0, role: "camp", home: "fire", tags: ["guy", "fly"],
pts: [{ x: 1.0, z: 2.0 }, { x: 3.5, z: -4 }], at: { x: 5 }, pick: fn twice }
bench { model: MDL_BENCH, scale: G_SCALE, lit: true, home: "" }
sign { model: 3, role: "say \"hi\"" }

View file

@ -0,0 +1,2 @@
fire { kind: "camp", x: 1.0 }
view { kind: "view", x: -2.5 }

View file

@ -0,0 +1,2 @@
# a field PropRow does not have (a map one directory down, which --check does not read as a map)
lamp { model: 1, colour: 3 }

View file

@ -0,0 +1,2 @@
# fewer rows than alpha: a reload shrinks the table in place
crate { model: 12, pts: [{ x: 9.0 }] }

View file

@ -0,0 +1 @@
dock { kind: "work" }

View file

@ -0,0 +1,4 @@
# the map-scoped tables' example (permap.ludic): its maps are the directories under maps/, and its
# frame code is held to the arena's region rule - the generated verbs keep nothing a frame makes
maps "maps"
arena strict

View file

@ -0,0 +1,86 @@
# permap.ludic — map-scoped tables: a @PerMap registry's rows are not compiled in, they are read
# from `<maps root>/<map>/<file>` when a map loads (package.ludic says `maps "maps"`), and a
# @Chunked one a chunk at a time. The compiler writes the table's state and its verbs - props_load,
# props_find, props_path; instances_in, _out, _slot, _find - and pools every row, list and record, so
# a reload refills them in place and allocates nothing past the table's high water.
#
# Running it prints: yes yes 3 7 90 2 -4 fly 5 42 0 9 3 yes 8 / yes 1 -1 0 crate 12 1 0 0 21 same examples/lang/permap/maps/beta/density/a.png / 0 1 2 2 7 1 0 1 1 / -1 1 0 2 3 / no 0 examples/lang/permap/maps/bad/one/props.lres:2:18: PropRow has no field colour
program PerMap {
numbers float
const MDL_TENT: int = 7
const MDL_BENCH: int = MDL_TENT + 2
const G_SCALE: float = 1.5
property Pt { x: float = 0.0, z: float = 0.0 }
# a place on the map, found by key
property Spot {
key: string = ""
@OneOf("camp", "view", "work") kind: string = "view"
x: float = 0.0
}
@PerMap @ByKey registry Spots of Spot from "spots.lres"
# a prop placed on the map
property PropRow {
key: string = ""
model: int = 0
@Unit("deg") yaw: float = 0.0
scale: float = 1.0
lit: bool = false
role: string = ""
@Ref(Spots) home: string = ""
pts: []Pt = new []Pt
tags: []string = new []string
at: Pt = new Pt
pick: fn(int) -> int = fn keep_n
}
@PerMap @ByKey registry Props of PropRow from "props.lres"
property InstRow { key: string = "", kind: int = 0, x: float = 0.0, z: float = 0.0 }
@PerMap @Chunked(64) registry Instances of InstRow from "instances/{cx}_{cz}.lres"
# a frame's streaming: the ring moved, a chunk comes in and one goes out - frame code, which the
# region rule (`arena strict` in package.ludic) holds to keeping nothing it makes
event Stream { cx: int = 0, cz: int = 0 }
@On(Stream) handler Streamed(inst: mut Instances, props: Props) {
let s = instances_in(inst, "alpha", cx, cz)
if s >= 0 and props_find(props, "tent") < 0 { instances_out(inst, cx, cz) }
}
function keep_n(n: int) -> int { return n }
function twice(n: int) -> int { return n * 2 }
function yn(b: bool) -> string {
if b { return "yes" }
return "no"
}
entry (props: mut Props, spots: mut Spots, inst: mut Instances) {
# a whole-map table: constants by name, a nested list, a cross-row @Ref found by its key
let a = props_load(props, "alpha")
let s = spots_load(spots, "alpha")
let t = props.rows[props_find(props, "tent")]
let pick = t.pick
let bench = props.rows[props_find(props, "bench")]
let sign = props.rows[2]
var out = `{yn(a)} {yn(s)} {len(props.rows)} {t.model} {int(t.yaw)} {len(t.pts)} {int(t.pts[1].z)} {t.tags[1]} {int(t.at.x)} {pick(21)} {spots_find(spots, t.home)}`
out = out + ` {bench.model} {int(bench.scale * 2.0)} {yn(bench.lit)} {len(sign.role)}`
# a reload with fewer rows: row 0's record is reused, reset and refilled - no stale row, no stale list
let kept = props.rows[0]
let b = props_load(props, "beta")
let c = props.rows[0]
var same = "new"
if c == kept { same = "same" }
let pick2 = c.pick
out = out + ` / {yn(b)} {len(props.rows)} {props_find(props, "tent")} {props_find(props, "crate")} {c.key} {c.model} {len(c.pts)} {len(c.tags)} {len(c.role)} {pick2(21)} {same} {props_path(props, "density/a.png")}`
# a chunked table: a negative and a positive chunk, and one with no file (an empty chunk)
let s1 = instances_in(inst, "alpha", -1, 0)
let s2 = instances_in(inst, "alpha", 0, 2)
let s3 = instances_in(inst, "alpha", 5, 5)
out = out + ` / {s1} {s2} {s3} {len(inst.chunks[s1].rows)} {inst.chunks[s1].rows[0].kind} {len(inst.chunks[s2].rows)} {len(inst.chunks[s3].rows)} {instances_find(inst, s1, "t2")} {instances_slot(inst, 0, 2)}`
# out and in again: the free slot is taken back, nothing new is made
instances_out(inst, -1, 0)
let gone = instances_slot(inst, -1, 0)
let s4 = instances_in(inst, "alpha", 0, 2)
let s5 = instances_in(inst, "alpha", -1, 0)
out = out + ` / {gone} {s4} {s5} {len(inst.chunks[s5].rows)} {len(inst.chunks)}`
# a wrong file: false, an empty table, and the error at its line and column
let bad = props_load(props, "bad/one")
out = out + ` / {yn(bad)} {len(props.rows)} {props.err}`
print(out)
emit Stream(cx: 0, cz: 2)
}
}

View file

@ -0,0 +1,5 @@
tent { model: MDL_TENT, home: "fire" }
lamp { model: 1, colour: 3 }
crate { model: "big" }
bench { model: MDL_BENCH }
sign { model: 2, scale: 0.5, home: "nowhere" }

View file

@ -0,0 +1 @@
fire { x: 1.0 }

View file

@ -0,0 +1,2 @@
# permap_maps.ludic's maps, each file with one thing `ludicc --check` must refuse
maps "maps"

View file

@ -0,0 +1,19 @@
# permap_maps.ludic - `ludicc --check` reads every map directory and checks its tables against their
# records. maps/m1 holds four mistakes, each refused at its file, line and column: a field the
# record lacks, a value of the wrong type, a constant nobody declared, and a key naming no row.
program PerMapMaps {
numbers float
const MDL_TENT: int = 7
property Spot { key: string = "", x: float = 0.0 }
@PerMap registry Spots of Spot from "spots.lres"
property PropRow {
key: string = ""
model: int = 0
scale: float = 1.0
@Ref(Spots) home: string = ""
}
@PerMap @ByKey registry Props of PropRow from "props.lres"
entry (props: mut Props) {
print(`{props_load(props, "m1")}`)
}
}

View file

@ -0,0 +1,7 @@
# permap_prefix.ludic - a @PerMap registry's keys are not known when the program compiles, so it has
# no `as PREFIX` and no constants
program PerMapPrefix {
property Spot { key: string = "" }
@PerMap registry Spots of Spot as SPOT from "spots.lres"
entry { print(1) }
}

View file

@ -0,0 +1,12 @@
# permap_ref_int.ludic - an @Ref into a @PerMap registry names a row by its key: a map's rows have
# no index a program could hold, so the field is a string
program PerMapRefInt {
property Spot { key: string = "" }
@PerMap registry Spots of Spot from "spots.lres"
property Npc {
key: string = ""
@Ref(Spots) home: int = 0
}
@PerMap registry Npcs of Npc from "npcs.lres"
entry { print(1) }
}

View file

@ -0,0 +1,6 @@
# permap_unit.ludic - @Unit's spellings are ASCII, one per unit: "°" builds, with a warning naming
# "deg" (it lives among the rejected examples for the warning it draws, not for an error)
program PerMapUnit {
property Sign { @Unit("°") yaw: float = 0.0, @Unit("m/s") speed: float = 0.0 }
entry { print(1) }
}

View file

@ -62,6 +62,17 @@ function schema_case(path: pointer, want: pointer, label: pointer) -> void {
}
if s_contains(capture(`cat {js}`), want) { ok(label) } else { bad2(label, `the schema has no [{want}]`) }
}
# @PerMap: --check reads the example's maps and finds them clean; --no-maps leaves a broken map alone;
# a @Unit spelled another way is a warning naming the canonical one
function permap_check_case() -> void {
let lbl = "--check: every map of lang/permap is clean, --no-maps skips a broken one, @Unit(\"°\") warns \"deg\""
let js = `{tmp_dir()}/dj_permap.json`
if not shq(`bin/ludicc examples/lang/permap/permap.ludic --check --diagnostics=json > {js} 2>/dev/null`) { bad2(lbl, capture_line(`head -3 {js}`)); return }
if not shq(`bin/ludicc examples/rejected/permap_maps/permap_maps.ludic --check --no-maps > /dev/null 2>&1`) { bad2(lbl, "--no-maps still read the maps"); return }
if not shq(`bin/ludicc examples/rejected/permap_unit.ludic --check --diagnostics=json > {js} 2>/dev/null`) { bad2(lbl, "a unit spelling was refused, not warned"); return }
if not s_contains(capture(`cat {js}`), "@Unit(\"°\"): the unit is spelled \"deg\"") { bad2(lbl, capture_line(`head -3 {js}`)); return }
ok(lbl)
}
# --check --diagnostics=json: refused, with exactly `n` diagnostics on stdout, one of them holding `want`
function diag_json_case(path: pointer, n: int, want: pointer, label: pointer) -> void {
let js = `{tmp_dir()}/dj_{flat(path)}.json`
@ -1256,6 +1267,17 @@ function cmd_dev_test() -> int {
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_missing", "cannot read the resource file data/no_such.lres", "a registry's resource file must exist")
feat_case("lang/permap/permap", "", "yes yes 3 7 90 2 -4 fly 5 42 0 9 3 yes 8 / yes 1 -1 0 crate 12 1 0 0 21 same examples/lang/permap/maps/beta/density/a.png / 0 1 2 2 7 1 0 1 1 / -1 1 0 2 3 / no 0 examples/lang/permap/maps/bad/one/props.lres:2:18: PropRow has no field colour", "permap.ludic (@PerMap: rows read per map - constants and fn by name, nested lists, a cross-row @Ref, a reload shrinking in place, chunks in/out/in, a missing chunk empty, a bad file false + file:line:col; built under arena strict)")
permap_check_case()
schema_case("lang/permap/permap", "\"prefix\": null, \"source\": \"instances/{cx}_{cz}.lres\", \"open\": false, \"exported\": false, \"module\": null, \"scope\": \"map\", \"chunk\": 64", "--emit-schema: a @PerMap registry has scope map, its chunk size and no prefix")
schema_case("lang/permap/permap", "{\"name\": \"Ref\", \"args\": [\"Spots\"], \"scope\": \"map\"}", "--emit-schema: an @Ref into a map table says scope map")
schema_case("lang/permap/permap", "\"units\": [\"m\", \"m/s\", \"m/s2\"", "--emit-schema: the canonical @Unit spellings")
reject_case("rejected/permap_ref_int", "names a @PerMap registry, whose rows have no index a program can hold - use a string key", "@Ref into a @PerMap registry is on a string field")
reject_case("rejected/permap_prefix", "a @PerMap registry has no 'as PREFIX'", "a @PerMap registry has no prefix and no constants")
diag_json_case("rejected/permap_maps/permap_maps", 4, "\"line\": 2, \"col\": 18, \"severity\": \"error\", \"message\": \"PropRow has no field colour\"", "--check reads every map: a field its record lacks, at the map file's line and column")
diag_json_case("rejected/permap_maps/permap_maps", 4, "model wants an int, not a string", "--check reads every map: a value of the wrong type")
diag_json_case("rejected/permap_maps/permap_maps", 4, "unknown constant MDL_BENCH", "--check reads every map: a constant nobody declared")
diag_json_case("rejected/permap_maps/permap_maps", 4, "there is no Spots row nowhere in this map", "--check reads every map: a cross-row @Ref names a row of the same map")
feat_case("lang/defaults", "", "a 300.0 8.0 / b 120.0 8.0 / c! 300.0 8.0 / d 300.0 2.0 / x / y", "defaults.ludic (L11: default parameters, positional then named arguments, in a generic too)")
reject_case("rejected/default_missing", "this call to f leaves out a, which has no default", "a call leaves out only what has a default")
reject_case("rejected/default_unknown", "f has no parameter c", "a named argument names a parameter the function has")