From 5a751434ac383432f80e496b98dd244e2f258bd9 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 29 Sep 2026 21:03:02 +0300 Subject: [PATCH] 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 --- .../permap/maps/alpha/instances/-1_0.lres | 3 + .../lang/permap/maps/alpha/instances/0_2.lres | 1 + examples/lang/permap/maps/alpha/props.lres | 6 ++ examples/lang/permap/maps/alpha/spots.lres | 2 + examples/lang/permap/maps/bad/one/props.lres | 2 + examples/lang/permap/maps/beta/props.lres | 2 + examples/lang/permap/maps/beta/spots.lres | 1 + examples/lang/permap/package.ludic | 4 + examples/lang/permap/permap.ludic | 86 +++++++++++++++++++ .../rejected/permap_maps/maps/m1/props.lres | 5 ++ .../rejected/permap_maps/maps/m1/spots.lres | 1 + examples/rejected/permap_maps/package.ludic | 2 + .../rejected/permap_maps/permap_maps.ludic | 19 ++++ examples/rejected/permap_prefix.ludic | 7 ++ examples/rejected/permap_ref_int.ludic | 12 +++ examples/rejected/permap_unit.ludic | 6 ++ tools/ludic-cli/test.ludic | 22 +++++ 17 files changed, 181 insertions(+) create mode 100644 examples/lang/permap/maps/alpha/instances/-1_0.lres create mode 100644 examples/lang/permap/maps/alpha/instances/0_2.lres create mode 100644 examples/lang/permap/maps/alpha/props.lres create mode 100644 examples/lang/permap/maps/alpha/spots.lres create mode 100644 examples/lang/permap/maps/bad/one/props.lres create mode 100644 examples/lang/permap/maps/beta/props.lres create mode 100644 examples/lang/permap/maps/beta/spots.lres create mode 100644 examples/lang/permap/package.ludic create mode 100644 examples/lang/permap/permap.ludic create mode 100644 examples/rejected/permap_maps/maps/m1/props.lres create mode 100644 examples/rejected/permap_maps/maps/m1/spots.lres create mode 100644 examples/rejected/permap_maps/package.ludic create mode 100644 examples/rejected/permap_maps/permap_maps.ludic create mode 100644 examples/rejected/permap_prefix.ludic create mode 100644 examples/rejected/permap_ref_int.ludic create mode 100644 examples/rejected/permap_unit.ludic diff --git a/examples/lang/permap/maps/alpha/instances/-1_0.lres b/examples/lang/permap/maps/alpha/instances/-1_0.lres new file mode 100644 index 00000000..c23935e7 --- /dev/null +++ b/examples/lang/permap/maps/alpha/instances/-1_0.lres @@ -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 } diff --git a/examples/lang/permap/maps/alpha/instances/0_2.lres b/examples/lang/permap/maps/alpha/instances/0_2.lres new file mode 100644 index 00000000..5b940f7f --- /dev/null +++ b/examples/lang/permap/maps/alpha/instances/0_2.lres @@ -0,0 +1 @@ +r1 { kind: 1, x: 3.0, z: 130.0 } diff --git a/examples/lang/permap/maps/alpha/props.lres b/examples/lang/permap/maps/alpha/props.lres new file mode 100644 index 00000000..a4f2d82b --- /dev/null +++ b/examples/lang/permap/maps/alpha/props.lres @@ -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\"" } diff --git a/examples/lang/permap/maps/alpha/spots.lres b/examples/lang/permap/maps/alpha/spots.lres new file mode 100644 index 00000000..02cd598e --- /dev/null +++ b/examples/lang/permap/maps/alpha/spots.lres @@ -0,0 +1,2 @@ +fire { kind: "camp", x: 1.0 } +view { kind: "view", x: -2.5 } diff --git a/examples/lang/permap/maps/bad/one/props.lres b/examples/lang/permap/maps/bad/one/props.lres new file mode 100644 index 00000000..b972b8eb --- /dev/null +++ b/examples/lang/permap/maps/bad/one/props.lres @@ -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 } diff --git a/examples/lang/permap/maps/beta/props.lres b/examples/lang/permap/maps/beta/props.lres new file mode 100644 index 00000000..c3d0eeb9 --- /dev/null +++ b/examples/lang/permap/maps/beta/props.lres @@ -0,0 +1,2 @@ +# fewer rows than alpha: a reload shrinks the table in place +crate { model: 12, pts: [{ x: 9.0 }] } diff --git a/examples/lang/permap/maps/beta/spots.lres b/examples/lang/permap/maps/beta/spots.lres new file mode 100644 index 00000000..625fef2c --- /dev/null +++ b/examples/lang/permap/maps/beta/spots.lres @@ -0,0 +1 @@ +dock { kind: "work" } diff --git a/examples/lang/permap/package.ludic b/examples/lang/permap/package.ludic new file mode 100644 index 00000000..33b8dda7 --- /dev/null +++ b/examples/lang/permap/package.ludic @@ -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 diff --git a/examples/lang/permap/permap.ludic b/examples/lang/permap/permap.ludic new file mode 100644 index 00000000..1033764c --- /dev/null +++ b/examples/lang/permap/permap.ludic @@ -0,0 +1,86 @@ +# permap.ludic — map-scoped tables: a @PerMap registry's rows are not compiled in, they are read +# from `//` 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) + } +} diff --git a/examples/rejected/permap_maps/maps/m1/props.lres b/examples/rejected/permap_maps/maps/m1/props.lres new file mode 100644 index 00000000..29c5a700 --- /dev/null +++ b/examples/rejected/permap_maps/maps/m1/props.lres @@ -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" } diff --git a/examples/rejected/permap_maps/maps/m1/spots.lres b/examples/rejected/permap_maps/maps/m1/spots.lres new file mode 100644 index 00000000..b144b9c3 --- /dev/null +++ b/examples/rejected/permap_maps/maps/m1/spots.lres @@ -0,0 +1 @@ +fire { x: 1.0 } diff --git a/examples/rejected/permap_maps/package.ludic b/examples/rejected/permap_maps/package.ludic new file mode 100644 index 00000000..261e2e18 --- /dev/null +++ b/examples/rejected/permap_maps/package.ludic @@ -0,0 +1,2 @@ +# permap_maps.ludic's maps, each file with one thing `ludicc --check` must refuse +maps "maps" diff --git a/examples/rejected/permap_maps/permap_maps.ludic b/examples/rejected/permap_maps/permap_maps.ludic new file mode 100644 index 00000000..19463950 --- /dev/null +++ b/examples/rejected/permap_maps/permap_maps.ludic @@ -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")}`) + } +} diff --git a/examples/rejected/permap_prefix.ludic b/examples/rejected/permap_prefix.ludic new file mode 100644 index 00000000..e2c3d3f5 --- /dev/null +++ b/examples/rejected/permap_prefix.ludic @@ -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) } +} diff --git a/examples/rejected/permap_ref_int.ludic b/examples/rejected/permap_ref_int.ludic new file mode 100644 index 00000000..25a3b0ca --- /dev/null +++ b/examples/rejected/permap_ref_int.ludic @@ -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) } +} diff --git a/examples/rejected/permap_unit.ludic b/examples/rejected/permap_unit.ludic new file mode 100644 index 00000000..f19fcb9b --- /dev/null +++ b/examples/rejected/permap_unit.ludic @@ -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) } +} diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index 772b7c35..eb923593 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -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")