@PerMap and @Chunked(n) registries: rows read per map, a state and verbs written, every map checked

- @PerMap registry R of T from "file.lres" reads <maps root>/<map>/file.lres at run time
  (package.ludic's new `maps` line, default assets/maps); @Chunked(n) one file per chunk,
  {cx}/{cz} in its name. No as PREFIX, no constants, no def, never open.
- permap_gen: state R, r_load/_clear/_find/_path; chunked: RChunk, r_in/_out/_slot/_find/
  _clear/_path; a typed reset and fill per record over lres.ludic, rows, lists and list
  records pooled per table / chunk slot, @alloc_ok on what grows at high water.
- permap_consts: lres_consts__(), the program's int and float constants by name.
- permap_check: ludicc --check reads every map directory (fields, types, constants, fn,
  @OneOf/@Range/@Ref, cross-row @Ref keys in the same map, a key twice); --no-maps skips.
- @Ref(PerMapTable) is refused on a non-string field; the schema says scope/chunk per
  registry and scope map on such a Ref, and lists the canonical @Unit spellings; any other
  @Unit spelling is a warning naming the canonical one.
- reseeded (mac + win seeds; bootstrap-cfree out.ll == seed.ll).

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 7170c31304
commit 2f4860ae71
14 changed files with 137400 additions and 100393 deletions

View file

@ -136,6 +136,7 @@ function at_field(g: int, a: pointer) -> void {
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")`) }
if a == "Unit" { at_unit_check(g_at_arg0, toks[pi - 2]) }
if at_flag(a) and n != 0 { perr(`@{a} takes nothing`) }
at_push(g, a, args, g_at_arg0, n)
}
@ -144,16 +145,27 @@ function at_field_group(f: Node) -> int {
if f.at < 0 { f.at = at_group() }
return f.at
}
# @AppendOnly / @ByKey before a declaration: kept for the registry it is on
# @AppendOnly / @ByKey / @PerMap / @Chunked(64) before a declaration: kept for the registry it is on
var g_at_decl_args: []pointer = new []pointer
function at_decl(a: pointer) -> void {
at_decl_with(a, "[]")
}
function at_decl_with(a: pointer, args: pointer) -> void {
push(g_at_decl, a)
push(g_at_decl_args, args)
}
function at_decl_reset() -> void {
if g_at_keep {
g_at_keep = false
return
}
if len(g_at_decl) > 0 { g_at_decl = new []pointer }
if len(g_at_decl) > 0 { at_decl_clear() }
}
function at_decl_clear() -> void {
g_at_decl = new []pointer
g_at_decl_args = new []pointer
g_pm_pending = false # permap.ludic: @PerMap and @Chunked belong to one declaration
g_pm_chunk_pending = 0
}
# the registry just declared takes the attributes read before it
function at_registry_take() -> int {
@ -161,10 +173,16 @@ function at_registry_take() -> int {
let g = at_group()
var i = 0
while i < len(g_at_decl) {
at_push(g, g_at_decl[i], "[]", null, 0)
var first: pointer = null
var n = 0
if not (g_at_decl_args[i] == "[]") {
first = g_at_decl_args[i][1 .. len(g_at_decl_args[i]) - 1]
n = 1
}
at_push(g, g_at_decl[i], g_at_decl_args[i], first, n)
i += 1
}
g_at_decl = new []pointer
at_decl_clear()
return g
}
# once every registry is declared: each @Ref names one, each @Node / @Clip a glTF field and a
@ -191,6 +209,9 @@ 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] == "Ref") and pm_named(g_at_first[j]) and not (f.ty == "string") {
perr_at(f.file, f.line, f.col, `field {d.s}.{f.s}: @Ref({g_at_first[j]}) names a @PerMap registry, whose rows have no index a program can hold - use a string key: @Ref({g_at_first[j]}) {f.s}: string = ""`)
}
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]) }
@ -218,3 +239,50 @@ function at_const_target(d: Node, f: Node, j: int, x: pointer) -> void {
g_at_unres[j] = true
pwarn_at(f.file, f.line, f.col, `{where}: this program has no constant {x} - left unresolved`)
}
# ---- units ----------------------------------------------------------------------------------------
# @Unit's spellings are ASCII, one each, so an editor and a converter read one word for one unit:
# "deg" is the angle, never "°". Anything else is a warning naming the canonical spelling.
function at_units() -> []pointer {
let us = new []pointer
push(us, "m"); push(us, "m/s"); push(us, "m/s2"); push(us, "s"); push(us, "min"); push(us, "h"); push(us, "d")
push(us, "deg"); push(us, "rad"); push(us, "rad/s"); push(us, "kg"); push(us, "N"); push(us, "N.m"); push(us, "%"); push(us, "px")
return us
}
function at_unit_known(u: pointer) -> bool {
let us = at_units()
var i = 0
while i < len(us) {
if (us[i] == u) { return true }
i += 1
}
return false
}
# the canonical spelling of a unit written another way, or null when there is no obvious one
function at_unit_canon(u: pointer) -> pointer {
if (u == "°") or (u == "degrees") or (u == "degree") or (u == "degs") { return "deg" }
if (u == "sec") or (u == "secs") or (u == "second") or (u == "seconds") { return "s" }
if (u == "m/s^2") or (u == "m/s²") or (u == "m/s/s") { return "m/s2" }
if (u == "Nm") or (u == "N m") or (u == "N*m") or (u == "N·m") { return "N.m" }
if (u == "radians") or (u == "radian") { return "rad" }
if (u == "rad/sec") { return "rad/s" }
if (u == "meters") or (u == "metres") or (u == "meter") or (u == "metre") { return "m" }
if (u == "mps") or (u == "m/sec") { return "m/s" }
if (u == "minutes") or (u == "minute") or (u == "mins") { return "min" }
if (u == "hours") or (u == "hour") or (u == "hr") or (u == "hrs") { return "h" }
if (u == "days") or (u == "day") { return "d" }
if (u == "kilograms") or (u == "kilogram") or (u == "kgs") { return "kg" }
if (u == "percent") or (u == "pct") { return "%" }
if (u == "pixels") or (u == "pixel") { return "px" }
if (u == "newtons") or (u == "newton") { return "N" }
return null
}
function at_unit_check(u: pointer, t: Tok) -> void {
if u == null or at_unit_known(u) { return }
let c = at_unit_canon(u)
if c != null {
pwarn_at(g_parse_file, t.line, t.col, `@Unit("{u}"): the unit is spelled "{c}"`)
return
}
pwarn_at(g_parse_file, t.line, t.col, `@Unit("{u}") is not a known unit (m, m/s, m/s2, s, min, h, d, deg, rad, rad/s, kg, N, N.m, %, px)`)
}

View file

@ -1007,6 +1007,11 @@ function parse_one_decl() -> void {
else if a == "Namespace" { eat_op("("); ns_name = eat_id(); eat_op(")") } # @Namespace(Name) package Foo.* namespace (#62)
else if a == "deterministic" { is_det = true }
else if (a == "AppendOnly") or (a == "ByKey") { at_decl(a) } # a registry's, for editors (attrs.ludic)
else if a == "PerMap" { # a map-scoped registry (permap.ludic)
at_decl(a)
g_pm_pending = true
}
else if a == "Chunked" { pm_parse_chunked() } # @Chunked(64): read a chunk at a time
else if a == "alloc_ok" { # 25.2: @alloc_ok("why") - this allocates in a frame on purpose
eat_op("(")
let why = expr()
@ -1046,12 +1051,18 @@ function parse_one_decl() -> void {
g_at_keep = true
let p0 = len(prog)
let e0 = len(g_events)
let r0 = len(g_rg_var)
parse_one_decl()
var k = p0
while k < len(prog) {
prog[k].vis = 1
k += 1
}
k = r0
while k < len(g_rg_var) { # a @PerMap registry's own node is not in prog
g_rg_var[k].vis = 1
k += 1
}
k = e0
while k < len(g_events) {
g_events[k].vis = 1
@ -1530,6 +1541,12 @@ function maybe_splice_runtime() -> void {
do_import("runtime/native/udp.ludic")
cur_dir = saved
}
# a @PerMap registry: the .lres reader its generated fill calls (permap.ludic)
if g_pm_any {
cur_dir = ""
do_import("runtime/native/lres.ludic")
cur_dir = saved
}
# Process.*: splice the child-process library. Spawning is native (process.ll /
# process_win.ll); the argv array and the Windows command line are built in Ludic.
if g_uses_process {

View file

@ -0,0 +1,309 @@
# permap.ludic — map-scoped tables: a registry whose rows are read when a map loads.
#
# @PerMap @ByKey registry Props of PropRow from "props.lres"
# @PerMap @Chunked(64) registry Instances of InstRow from "instances/{cx}_{cz}.lres"
#
# Its entries are not compiled in: they live under `<maps root>/<map>/`, one directory a map (the
# root is package.ludic's `maps "assets/maps"`), and are read at run time by runtime/native/lres.ludic,
# which is spliced when a program declares one. So it has no `as PREFIX` and no constants - a row
# is found by its key. What the compiler writes instead, in the declaring module and exported with
# the registry (permap_gen.ludic): a state named after the registry, its verbs (props_load,
# props_clear, props_find, props_path; instances_in / _out / _slot / _find / _clear / _path) and a
# typed fill for the record, every row, list and record of it pooled so a load allocates nothing
# past the table's high water. `ludicc --check` reads every map directory there is and checks its
# files against the record (permap_check.ludic).
var g_pm_pending: bool = false # @PerMap was read before the declaration being parsed
var g_pm_chunk_pending: int = 0 # ... and @Chunked(n)
var g_rg_permap: []bool = new []bool # per registry: map-scoped
var g_rg_chunk: []int = new []int # per registry: its chunk's size in metres, 0 for a whole-map table
var g_pm_any: bool = false # a @PerMap registry exists: lres.ludic is spliced
var g_pm_root: pointer = "assets/maps" # where the maps are, as the program opens it
var g_pm_no_maps: bool = false # --no-maps: --check leaves the map directories alone
# `@Chunked(64)` before a declaration, its '@' and name read
function pm_parse_chunked() -> void {
eat_op("(")
let e = expr()
eat_op(")")
if e.kind != E_INT or e.ival <= 0 { perr("@Chunked takes a chunk's size in metres: @Chunked(64)") }
g_pm_chunk_pending = e.ival
at_decl_with("Chunked", `[{itoa(e.ival)}]`)
}
# a registry just declared: is it map-scoped, and how big are its chunks
function pm_declared(pm: bool, chunk: int, name: pointer) -> void {
push(g_rg_permap, pm)
push(g_rg_chunk, chunk)
if chunk > 0 and not pm { perr(`registry {name}: @Chunked is for a @PerMap registry - @PerMap @Chunked({itoa(chunk)}) registry {name} ...`) }
}
function pm_is(r: int) -> bool { return r >= 0 and r < len(g_rg_permap) and g_rg_permap[r] }
function pm_named(name: pointer) -> bool { return pm_is(reg_find(name)) }
# the registry's name as its verbs' prefix: GroundLayers -> ground_layers, NPCKinds -> npc_kinds
function pm_snake(s: pointer) -> pointer {
let b = buf_new()
let n = len(s)
var i = 0
while i < n {
let c = s[i]
let up = c >= 'A' and c <= 'Z'
if up and i > 0 {
let p = s[i - 1]
let plow = (p >= 'a' and p <= 'z') or (p >= '0' and p <= '9')
var nlow = false
if i + 1 < n { nlow = s[i + 1] >= 'a' and s[i + 1] <= 'z' }
let pup = p >= 'A' and p <= 'Z'
if plow or (pup and nlow) { buf_putc(b, '_') }
}
if up { buf_putc(b, c + 32) } else { buf_putc(b, c) }
i += 1
}
return buf_str(b)
}
# package.ludic's `maps "assets/maps"`, looked for as the fence's lines are - the nearest package.ludic
# up to four directories above the program - and taken relative to that file's directory
function pm_manifest(entry: pointer) -> void {
var dir = dir_of(entry)
var up = 0
while up < 5 {
var root = dir
if len(root) > 0 and not is_sep(root[len(root) - 1]) { root = root + "/" }
let txt = read_file(root + "package.ludic")
if txt != null {
g_pm_root = pm_norm(join_path(root, "assets/maps"))
pm_manifest_read(txt, root)
return
}
if len(dir) == 0 { dir = ".." } else { dir = dir + "/.." }
up += 1
}
}
function pm_manifest_read(txt: pointer, root: pointer) -> void {
var i = 0
let tn = len(txt)
while i < tn {
var e = i
while (e < tn) and (txt[e] != '\n') { e += 1 }
var le = e
if (le > i) and (txt[le - 1] == 13) { le -= 1 }
let ws = native_words(txt[i..le])
if (len(ws) == 2) and (ws[0] == "maps") { g_pm_root = pm_norm(join_path(root, pm_trim_slash(ws[1]))) }
i = e + 1
}
}
# a path with its `.` and `x/..` steps taken out and '/' between them: the name the asset pack
# knows a file by is "assets/maps/...", never "src/../assets/maps/..."
function pm_norm(p: pointer) -> pointer {
let parts = new []pointer
var abs = len(p) > 0 and is_sep(p[0])
var i = 0
let n = len(p)
while i <= n {
var j = i
while j < n and not is_sep(p[j]) { j += 1 }
let w = p[i .. j]
if len(w) == 0 or (w == ".") { }
else if (w == "..") and len(parts) > 0 and not (parts[len(parts) - 1] == "..") { let dropped = List.pop(parts) }
else { push(parts, w) }
i = j + 1
}
var out: pointer = ""
if abs { out = "/" }
var k = 0
while k < len(parts) {
if k > 0 { out = out + "/" }
out = out + parts[k]
k += 1
}
if len(out) == 0 { return "." }
return out
}
function pm_trim_slash(s: pointer) -> pointer {
var n = len(s)
while n > 1 and is_sep(s[n - 1]) { n -= 1 }
return s[0 .. n]
}
# ---- the declaration's rules ----------------------------------------------------------------------
const PM_BAD: int = 0
const PM_INT: int = 1
const PM_FLOAT: int = 2
const PM_BOOL: int = 3
const PM_STR: int = 4
const PM_REC: int = 5
const PM_LIST: int = 6
const PM_FN: int = 7
function pm_kind(ty: pointer) -> int {
if ty == null { return PM_BAD }
if (ty == "int") { return PM_INT }
if (ty == "float") { return PM_FLOAT }
if (ty == "bool") { return PM_BOOL }
if (ty == "string") { return PM_STR }
if str_starts(ty, "fn(") { return PM_FN }
if is_slice_ty(ty) { return PM_LIST }
let c = find_comp(ty)
if c != null and c.tps == null and c.uns != 2 { return PM_REC }
return PM_BAD
}
# the record types a table's rows reach (its own first), whether each is a list's element (and so
# pooled), and the fn types its fields hold (each gets a resolver) - checked as they are found
var g_pm_recs: []pointer = new []pointer
var g_pm_pooled: []bool = new []bool
var g_pm_fnty: []pointer = new []pointer
function pm_rec_index(ty: pointer) -> int {
var i = 0
while i < len(g_pm_recs) {
if (g_pm_recs[i] == ty) { return i }
i += 1
}
return -1
}
function pm_fn_index(ty: pointer) -> int {
var i = 0
while i < len(g_pm_fnty) {
if (g_pm_fnty[i] == ty) { return i }
i += 1
}
return -1
}
function pm_add_rec(ty: pointer, pooled: bool) -> void {
let i = pm_rec_index(ty)
if i >= 0 {
if pooled { g_pm_pooled[i] = true }
return
}
push(g_pm_recs, ty)
push(g_pm_pooled, pooled)
}
function pm_collect(r: int) -> void {
g_pm_recs = new []pointer
g_pm_pooled = new []bool
g_pm_fnty = new []pointer
pm_add_rec(g_rg_type[r], false)
var k = 0
while k < len(g_pm_recs) {
let c = find_comp(g_pm_recs[k])
var i = 0
while i < len(c.kids) {
pm_collect_field(r, c, c.kids[i])
i += 1
}
k += 1
}
}
function pm_collect_field(r: int, c: Node, f: Node) -> void {
let k = pm_kind(f.ty)
let where = `field {c.s}.{f.s} of @PerMap registry {g_rg_name[r]}`
if k == PM_BAD {
perr_at(f.file, f.line, f.col, `{where}: a map's row holds an int, a float, a bool, a string, a record, a list of those or a fn value, not {f.ty}`)
return
}
if k == PM_REC and ((f.ty == c.s) or pm_single_reaches(f.ty, c.s, 0)) {
perr_at(f.file, f.line, f.col, `{where}: a record in a map's row holds itself only through a list - {c.s} would be made without end`)
return
}
if k == PM_REC { pm_add_rec(f.ty, false) }
if k == PM_FN and pm_fn_index(f.ty) < 0 { push(g_pm_fnty, f.ty) }
if k == PM_LIST {
let ek = pm_kind(slice_elem(f.ty))
if ek == PM_BAD or ek == PM_LIST or ek == PM_FN {
perr_at(f.file, f.line, f.col, `{where}: a list in a map's row holds ints, floats, bools, strings or records, not {slice_elem(f.ty)}`)
return
}
if ek == PM_REC { pm_add_rec(slice_elem(f.ty), true) }
}
}
# does record ty reach record target through its record fields (not through a list)?
function pm_single_reaches(ty: pointer, target: pointer, depth: int) -> bool {
if depth > 16 { return true }
let c = find_comp(ty)
if c == null { return false }
var i = 0
while i < len(c.kids) {
let f = c.kids[i]
if pm_kind(f.ty) == PM_REC {
if (f.ty == target) or pm_single_reaches(f.ty, target, depth + 1) { return true }
}
i += 1
}
return false
}
# what a @PerMap declaration may not be, said at the declaration
function pm_validate(r: int) -> bool {
let v = g_rg_var[r]
let name = g_rg_name[r]
let before = g_dg_errors
if g_rg_open[r] == 1 { perr_at(v.file, v.line, v.col, `registry {name}: a @PerMap registry is not open - its rows come from each map's files, not from defs`) }
let from = g_rg_from[r]
if from == null {
perr_at(v.file, v.line, v.col, `registry {name}: a @PerMap registry says which file under each map holds it - @PerMap registry {name} of {g_rg_type[r]} from "{pm_snake(name)}.lres"`)
return false
}
let chunked = g_rg_chunk[r] > 0
let has = has_sub(from, "{cx}") and has_sub(from, "{cz}")
let any = has_sub(from, "{cx}") or has_sub(from, "{cz}")
if chunked and not has { perr_at(v.file, v.line, v.col, `registry {name}: a @Chunked registry's file names its chunk - from "{pm_snake(name)}/{{cx}}_{{cz}}.lres"`) }
if not chunked and any { perr_at(v.file, v.line, v.col, `registry {name}: {{cx}} and {{cz}} name a chunk - declare it @Chunked(size)`) }
if chunked and pm_placeholder_in_dir(from) { perr_at(v.file, v.line, v.col, `registry {name}: {{cx}} and {{cz}} go in the file's own name, not in a directory above it`) }
let c = find_comp(g_rg_type[r])
if c == null {
perr_at(v.file, v.line, v.col, `registry {name}: there is no record {g_rg_type[r]}`)
return false
}
if c.tps != null { perr_at(v.file, v.line, v.col, `registry {name}: a @PerMap registry's record is not generic`) }
if len(c.kids) == 0 or not (c.kids[0].s == "key") or not (c.kids[0].ty == "string") {
perr_at(c.file, c.line, c.col, `record {c.s} of @PerMap registry {name}: its first field is the row's key - key: string = ""`)
return false
}
pm_collect(r)
return g_dg_errors == before
}
function pm_placeholder_in_dir(from: pointer) -> bool {
var last = -1
var i = 0
while i < len(from) {
if from[i] == '/' { last = i }
i += 1
}
if last < 0 { return false }
let dir = from[0 .. last]
return has_sub(dir, "{cx}") or has_sub(dir, "{cz}")
}
# a def into a map-scoped registry
function pm_def_check(d: int) -> void {
let r = reg_find(g_df_reg[d])
if not pm_is(r) { return }
g_err_file = g_df_rec[d].file
g_err_line = g_df_rec[d].line
g_parsing = false
perr(`def {g_rg_name[r]} {g_df_key[d]}: {g_rg_name[r]} is a @PerMap registry - its rows are read from each map's {g_rg_from[r]} when the map loads, not written with def`)
}
# every map-scoped registry, once the program is read: its state, verbs and fill
function pm_finish() -> void {
var any = false
var r = 0
while r < len(g_rg_name) {
if pm_is(r) { any = true }
r += 1
}
if not any { return }
g_pm_any = true
pm_manifest(g_act_main)
let ok = new []bool
r = 0
while r < len(g_rg_name) {
push(ok, pm_is(r) and pm_validate(r))
r += 1
}
diag_stop_if_errors()
pm_consts_gen() # permap_consts.ludic: the constants by name
r = 0
while r < len(g_rg_name) {
if ok[r] {
pm_collect(r)
pm_gen(r) # permap_gen.ludic
}
r += 1
}
}

View file

@ -0,0 +1,549 @@
# permap_check.ludic — `ludicc --check` reads every map there is, so a map can't ship a wrong field.
#
# Each directory under the maps root is a map. For each @PerMap registry, its file in each map (for
# a @Chunked one, every file its pattern matches, {cx} and {cz} any integers) is read with the
# compiler's own resource parser and every entry checked against the record, as a compiled table's
# entries are: the field exists, its value is of the field's type, a constant it names exists (and is
# one lres_consts__ carries), a `fn name` names a function of the field's type, and the attributes
# hold - @OneOf, @Range, an @Ref into a game registry in range, and an @Ref into another @PerMap
# registry names a row of that table in the same map (in any of its chunks). A key is written once
# in a file. Errors carry the map file's line and column and go through the diagnostics.
# `--no-maps` leaves the maps alone; no maps root, nothing to check.
# one error in a map's file, with its column in the text form too
function pmk_err(file: pointer, line: int, col: int, msg: pointer) -> void {
if g_diag_json {
diag_add(file, line, col, "error", msg)
return
}
let m = `{file}:{itoa(line)}:{itoa(col)}: error: {msg}\n`
file_write(file_stderr(), m, len(m))
g_dg_errors += 1
}
function pmk_err_at(n: Node, msg: pointer) -> void { pmk_err(n.file, n.line, n.col, msg) }
# ---- the directory ------------------------------------------------------------------------------
function pmk_hash(s: pointer) -> int {
var h = 5381
var i = 0
while i < len(s) {
h = (h * 33 + s[i]) & 16777215
i += 1
}
return h
}
# the names in dir: its directories (dirs) or its files, sorted as the shell lists them
function pmk_list(dir: pointer, dirs: bool) -> []pointer {
let out = new []pointer
var tmpd = getenv_or("TMPDIR", "/tmp")
if host_is_windows() { tmpd = fwd_slashes(getenv_or("TEMP", ".")) }
let tmp = `{ensure_slash(tmpd)}ludicc-maps-{itoa(pmk_hash(dir + g_act_main))}.txt`
if host_is_windows() {
var flag = "/a-d"
if dirs { flag = "/ad" }
run(`dir /b /on {flag} "{win_path(dir)}" > "{win_path(tmp)}" 2>nul`)
} else {
run(`ls -1p '{dir}' > '{tmp}' 2>/dev/null`)
}
let txt = read_file(tmp)
remove_file(tmp)
if txt == null { return out }
var i = 0
let n = len(txt)
while i < n {
var e = i
while e < n and txt[e] != '\n' { e += 1 }
var le = e
if le > i and txt[le - 1] == 13 { le -= 1 }
if le > i {
var w = txt[i .. le]
if host_is_windows() { push(out, w) }
else {
let isdir = w[len(w) - 1] == '/'
if isdir { w = w[0 .. len(w) - 1] }
if isdir == dirs { push(out, w) }
}
}
i = e + 1
}
return out
}
# is s an integer, as a chunk's coordinate is written: -3, 0, 12
function pmk_is_int(s: pointer) -> bool {
var i = 0
if len(s) > 1 and s[0] == '-' { i = 1 }
if i >= len(s) { return false }
while i < len(s) {
if s[i] < '0' or s[i] > '9' { return false }
i += 1
}
return true
}
# does the file name `name` fit the pattern's last part, {cx} and {cz} integers
function pmk_match(pat: pointer, name: pointer) -> bool {
var i = 0
var j = 0
while i < len(pat) {
if pat[i] == '{' and i + 3 < len(pat) and pat[i + 1] == 'c' and pat[i + 3] == '}' {
i += 4
# the integer runs to where the pattern's next literal byte is (or the end)
var k = j
if k < len(name) and name[k] == '-' { k += 1 }
while k < len(name) and name[k] >= '0' and name[k] <= '9' { k += 1 }
if not pmk_is_int(name[j .. k]) { return false }
j = k
} else {
if j >= len(name) or name[j] != pat[i] { return false }
i += 1
j += 1
}
}
return j == len(name)
}
# the files of registry r in the map directory md
function pmk_files(r: int, md: pointer) -> []pointer {
let out = new []pointer
let from = g_rg_from[r]
if g_rg_chunk[r] == 0 {
let p = `{md}/{from}`
if read_file(p) != null { push(out, p) }
return out
}
var last = -1
var i = 0
while i < len(from) {
if from[i] == '/' { last = i }
i += 1
}
var dir = md
var pat = from
if last >= 0 {
dir = `{md}/{from[0 .. last]}`
pat = from[last + 1 .. len(from)]
}
let names = pmk_list(dir, false)
i = 0
while i < len(names) {
if pmk_match(pat, names[i]) { push(out, `{dir}/{names[i]}`) }
i += 1
}
return out
}
# ---- one map's rows ---------------------------------------------------------------------------
var g_pmk_reg: []int = new []int # per entry read: its registry, key, record, file
var g_pmk_key: []pointer = new []pointer
var g_pmk_rec: []Node = new []Node
var g_pmk_kline: []int = new []int
var g_pmk_kcol: []int = new []int
var g_pmk_file: []pointer = new []pointer
var g_pmk_ref_reg: []pointer = new []pointer # per cross-row @Ref met: the table, the key, where
var g_pmk_ref_key: []pointer = new []pointer
var g_pmk_ref_at: []Node = new []Node
var g_pmk_mod: pointer = "" # the module of the registry being checked
function pmk_read(r: int, path: pointer) -> void {
let src = read_file(path)
if src == null { return }
let saved_toks = toks
let saved_pi = pi
let saved_file = g_parse_file
let saved_parsing = g_parsing
g_parse_file = path
g_parsing = true
g_res_mode = true
lex_at(src, 1)
pi = 0
while true {
skipnl()
if toks[pi].kind == TK_EOF { break }
let kt = toks[pi]
let key = eat_id()
skipnl()
let rec = record()
push(g_pmk_reg, r)
push(g_pmk_key, key)
push(g_pmk_rec, rec)
push(g_pmk_kline, kt.line)
push(g_pmk_kcol, kt.col)
push(g_pmk_file, path)
}
g_res_mode = false
toks = saved_toks
pi = saved_pi
g_parse_file = saved_file
g_parsing = saved_parsing
}
# every map directory under the root
function pm_check_maps() -> void {
if not g_pm_any or g_pm_no_maps { return }
let maps = pmk_list(g_pm_root, true)
var m = 0
while m < len(maps) {
pmk_map(`{g_pm_root}/{maps[m]}`)
m += 1
}
}
function pmk_map(md: pointer) -> void {
g_pmk_reg = new []int
g_pmk_key = new []pointer
g_pmk_rec = new []Node
g_pmk_kline = new []int
g_pmk_kcol = new []int
g_pmk_file = new []pointer
g_pmk_ref_reg = new []pointer
g_pmk_ref_key = new []pointer
g_pmk_ref_at = new []Node
var r = 0
while r < len(g_rg_name) {
if pm_is(r) {
let fs = pmk_files(r, md)
var i = 0
while i < len(fs) {
pmk_read(r, fs[i])
i += 1
}
}
r += 1
}
pmk_dups()
var e = 0
while e < len(g_pmk_rec) {
let rr = g_pmk_reg[e]
g_pmk_mod = module_of(g_rg_var[rr].file)
pmk_rec(g_pmk_rec[e], find_comp(g_rg_type[rr]), true)
e += 1
}
pmk_refs()
}
# a key written twice in one file
function pmk_dups() -> void {
let xs = new []int
let keys = new []pointer
var i = 0
while i < len(g_pmk_key) {
push(xs, i)
push(keys, `{g_pmk_file[i]}\n{g_pmk_key[i]}`)
i += 1
}
sc_sort(xs, keys)
i = 1
while i < len(xs) {
if (keys[xs[i]] == keys[xs[i - 1]]) {
let k = xs[i]
pmk_err(g_pmk_file[k], g_pmk_kline[k], g_pmk_kcol[k], `the key {g_pmk_key[k]} is written twice in this file`)
}
i += 1
}
}
# every cross-row @Ref met names a row of its table in this map
function pmk_refs() -> void {
let xs = new []int
let keys = new []pointer
var i = 0
while i < len(g_pmk_key) {
push(xs, i)
push(keys, `{g_rg_name[g_pmk_reg[i]]}\n{g_pmk_key[i]}`)
i += 1
}
sc_sort(xs, keys)
let sorted = new []pointer
i = 0
while i < len(xs) {
push(sorted, keys[xs[i]])
i += 1
}
i = 0
while i < len(g_pmk_ref_key) {
let want = `{g_pmk_ref_reg[i]}\n{g_pmk_ref_key[i]}`
if not pmk_sorted_has(sorted, want) {
pmk_err_at(g_pmk_ref_at[i], `there is no {g_pmk_ref_reg[i]} row {g_pmk_ref_key[i]} in this map`)
}
i += 1
}
}
function pmk_sorted_has(xs: []pointer, s: pointer) -> bool {
var lo = 0
var hi = len(xs) - 1
while lo <= hi {
let mid = (lo + hi) / 2
if (xs[mid] == s) { return true }
if reg_str_less(s, xs[mid]) { hi = mid - 1 } else { lo = mid + 1 }
}
return false
}
# ---- an entry against its record ----------------------------------------------------------------
function pmk_rec(rec: Node, comp: Node, row: bool) -> void {
var i = 0
while i < len(rec.kids) {
let fi = rec.kids[i]
let fx = field_index(comp, fi.s)
if fx < 0 {
pmk_err_at(fi, `{vis_plain(comp.s)} has no field {fi.s}`)
} else if row and (fi.s == "key") {
pmk_err_at(fi, "a row's key is its entry's name - drop the key field")
} else {
pmk_value(fi.a, comp.kids[fx], comp.kids[fx].ty, fi)
}
i += 1
}
}
function pmk_desc(e: Node) -> pointer {
if e.kind == E_INT or (e.kind == E_UN and e.a != null and e.a.kind == E_INT) { return "an int" }
if e.kind == E_FLOAT or (e.kind == E_UN and e.a != null and e.a.kind == E_FLOAT) { return "a float" }
if e.kind == E_STR { return "a string" }
if e.kind == E_BOOL { return "true or false" }
if e.kind == E_ID { return `the name {e.s}` }
if e.kind == E_REC { return "a record" }
if e.kind == E_LIST { return "a list" }
if e.kind == E_FNREF { return `fn {e.s}` }
return "an expression (a map's value is a literal, a constant's name or fn name)"
}
function pmk_want(e: Node, f: Node, what: pointer) -> void {
pmk_err_at(e, `{f.s} wants {what}, not {pmk_desc(e)}`)
}
# a number literal's text (with its sign), or null
function pmk_num(e: Node) -> pointer {
if e.kind == E_INT { return itoa(e.ival) }
if e.kind == E_FLOAT and e.s != null { return e.s }
if e.kind == E_UN and (e.s == "-") and e.a != null {
let t = pmk_num(e.a)
if t == null { return null }
if t[0] == '-' { return t[1 .. len(t)] }
return "-" + t
}
return null
}
# value e for field f of type ty; its attributes checked on each scalar
function pmk_value(e: Node, f: Node, ty: pointer, at: Node) -> void {
let k = pm_kind(ty)
if k == PM_REC {
if e.kind != E_REC { pmk_want(e, f, "a record { ... }"); return }
pmk_rec(e, find_comp(ty), false)
return
}
if k == PM_LIST {
if e.kind != E_LIST { pmk_want(e, f, "a list [ ... ]"); return }
var i = 0
while i < len(e.kids) {
pmk_value(e.kids[i], f, slice_elem(ty), at)
i += 1
}
return
}
if k == PM_FN {
if e.kind != E_FNREF { pmk_want(e, f, "fn name"); return }
let cs = pm_fn_cands(ty, g_pmk_mod)
var i = 0
while i < len(cs) {
if (vis_plain(cs[i].s) == e.s) { return }
i += 1
}
pmk_err_at(e, `there is no function {e.s} of type {ty} (field {f.s})`)
return
}
if k == PM_BOOL {
if e.kind != E_BOOL { pmk_want(e, f, "true or false") }
return
}
if k == PM_STR {
if e.kind != E_STR { pmk_want(e, f, "a \"string\""); return }
pmk_attrs_str(e, f)
return
}
# a number: a literal, or a constant by name
var text = pmk_num(e)
var cname: pointer = null
if text == null and e.kind == E_ID {
let c = pmc_lookup(e.s)
if c < 0 {
if pmc_find(e.s) != null { pmk_err_at(e, `the constant {e.s} is not one a map can name: its value is not a literal or a sum of them`) }
else { pmk_err_at(e, `unknown constant {e.s}`) }
return
}
if k == PM_INT and g_pmc_isf[c] {
pmk_err_at(e, `{e.s} is a float constant, and {f.s} wants an int`)
return
}
cname = e.s
if g_pmc_isf[c] { text = g_pmc_ftext[c] } else { text = itoa(g_pmc_iv[c]) }
}
if text == null {
if k == PM_INT { pmk_want(e, f, "an int") } else { pmk_want(e, f, "a float") }
return
}
if k == PM_INT and e.kind != E_ID and pmk_has_dot(text) {
pmk_want(e, f, "an int")
return
}
pmk_attrs_num(e, f, text, cname)
}
function pmk_has_dot(t: pointer) -> bool {
var i = 0
while i < len(t) {
if t[i] == '.' or t[i] == 'e' or t[i] == 'E' { return true }
i += 1
}
return false
}
# ---- attributes ---------------------------------------------------------------------------------
function pmk_attrs_str(e: Node, f: Node) -> void {
if f.at < 0 { return }
var j = g_at_start[f.at]
while j < g_at_start[f.at] + g_at_count[f.at] {
let a = g_at_name[j]
if (a == "OneOf") and g_at_strs[j] {
let words = at_arg_names(g_at_args[j])
if not at_word_in(e.s, words) { pmk_err_at(e, `{f.s} is "{e.s}", and it is @OneOf({at_words_text(words)})`) }
}
if (a == "Ref") and pm_named(g_at_first[j]) and len(e.s) > 0 {
push(g_pmk_ref_reg, g_at_first[j])
push(g_pmk_ref_key, e.s)
push(g_pmk_ref_at, e)
}
j += 1
}
}
function pmk_attrs_num(e: Node, f: Node, text: pointer, cname: pointer) -> void {
if f.at < 0 { return }
var j = g_at_start[f.at]
while j < g_at_start[f.at] + g_at_count[f.at] {
let a = g_at_name[j]
if a == "Range" {
let ns = pmk_arg_nums(g_at_args[j])
if len(ns) == 2 and (pmk_cmp(text, ns[0]) < 0 or pmk_cmp(text, ns[1]) > 0) {
pmk_err_at(e, `{f.s} is {text}, outside its @Range({ns[0]}, {ns[1]})`)
}
}
if (a == "Ref") and not g_at_unres[j] {
let r = reg_find(g_at_first[j])
if r >= 0 and not pm_is(r) {
let n = len(reg_order(r))
if pmk_cmp(text, "-1") < 0 or pmk_cmp(text, itoa(n)) >= 0 {
pmk_err_at(e, `{f.s} is {text}, and @Ref({g_rg_name[r]}) has {itoa(n)} entries (0 to {itoa(n - 1)}, or -1 for none)`)
}
}
}
if (a == "OneOf") and not g_at_strs[j] { pmk_oneof(e, f, j, text, cname) }
j += 1
}
}
# @OneOf(GR_) or @OneOf(A, B): a name must be one of them; a literal must be one's value
function pmk_oneof(e: Node, f: Node, j: int, text: pointer, cname: pointer) -> void {
let names = at_arg_names(g_at_args[j])
let first = g_at_first[j]
let prefix = g_at_nargs[j] == 1 and first[len(first) - 1] == '_'
if cname != null {
if prefix and str_starts(cname, first) { return }
if not prefix and at_word_in(cname, names) { return }
} else {
var i = 0
while i < len(g_pmc_names) {
var fits = false
if prefix { fits = str_starts(g_pmc_names[i], first) } else { fits = at_word_in(g_pmc_names[i], names) }
if fits and not g_pmc_isf[i] and (itoa(g_pmc_iv[i]) == text) { return }
i += 1
}
}
var shown = text
if cname != null { shown = cname }
var list = first
if not prefix { list = at_words_text(names) }
pmk_err_at(e, `{f.s} is {shown}, and it is @OneOf({list})`)
}
# @Range's two numbers, as written
function pmk_arg_nums(args: pointer) -> []pointer {
let out = new []pointer
var i = 1
let n = len(args)
while i < n {
while i < n and (args[i] == ' ' or args[i] == ',') { i += 1 }
let a = i
while i < n and args[i] != ',' and args[i] != ']' and args[i] != ' ' { i += 1 }
if i > a { push(out, args[a .. i]) }
if i < n and args[i] == ']' { return out }
}
return out
}
# two decimal numbers compared exactly, from their text: <0, 0, >0 (an exponent is not read)
function pmk_cmp(a: pointer, b: pointer) -> int {
let na = len(a) > 0 and a[0] == '-'
let nb = len(b) > 0 and b[0] == '-'
let za = pmk_zero(a)
let zb = pmk_zero(b)
var sa = 1
if na and not za { sa = -1 }
if za { sa = 0 }
var sb = 1
if nb and not zb { sb = -1 }
if zb { sb = 0 }
if sa != sb {
if sa < sb { return -1 }
return 1
}
if sa == 0 { return 0 }
let m = pmk_mag(a, b)
if sa < 0 { return 0 - m }
return m
}
function pmk_zero(t: pointer) -> bool {
var i = 0
while i < len(t) {
if t[i] >= '1' and t[i] <= '9' { return false }
if t[i] == 'e' or t[i] == 'E' { return true }
i += 1
}
return true
}
# |a| against |b|
function pmk_mag(a: pointer, b: pointer) -> int {
let ia = pmk_ipart(a)
let ib = pmk_ipart(b)
if len(ia) != len(ib) {
if len(ia) < len(ib) { return -1 }
return 1
}
var i = 0
while i < len(ia) {
if ia[i] != ib[i] {
if ia[i] < ib[i] { return -1 }
return 1
}
i += 1
}
let fa = pmk_fpart(a)
let fb = pmk_fpart(b)
i = 0
while i < len(fa) or i < len(fb) {
var ca = '0'
var cb = '0'
if i < len(fa) { ca = fa[i] }
if i < len(fb) { cb = fb[i] }
if ca != cb {
if ca < cb { return -1 }
return 1
}
i += 1
}
return 0
}
# the integer digits, leading zeros gone
function pmk_ipart(t: pointer) -> pointer {
var i = 0
if len(t) > 0 and (t[0] == '-' or t[0] == '+') { i = 1 }
while i < len(t) - 1 and t[i] == '0' and t[i + 1] >= '0' and t[i + 1] <= '9' { i += 1 }
var j = i
while j < len(t) and t[j] >= '0' and t[j] <= '9' { j += 1 }
return t[i .. j]
}
function pmk_fpart(t: pointer) -> pointer {
var i = 0
while i < len(t) and t[i] != '.' { i += 1 }
if i >= len(t) { return "" }
var j = i + 1
while j < len(t) and t[j] >= '0' and t[j] <= '9' { j += 1 }
return t[i + 1 .. j]
}

View file

@ -0,0 +1,182 @@
# permap_consts.ludic — a map's row names a constant (MDL_TENT2, SP_DEER, a registry's KEY) and it is
# resolved when the map loads. So a program with a @PerMap registry carries its int and float
# constants by name: `lres_consts__()` returns them as one text, "NAME i 12;NAME f 1.5;", sorted by
# name, which lres.ludic indexes once and searches in place. A constant is in it when the compiler
# can work out its value here - a literal, a sum or a shift of literals and other constants, a
# registry's index; anything else (a call, a field) is left out, and a row naming it fails to load
# as "unknown constant", as `ludicc --check` says it will. When two modules each have a private
# constant of one name, the first declared is the one a map reads.
var g_pmc_ival: int = 0
var g_pmc_fval: pointer = null
var g_pmc_names: []pointer = new []pointer # the table: plain name, whether a float, its value
var g_pmc_isf: []bool = new []bool
var g_pmc_iv: []int = new []int
var g_pmc_ftext: []pointer = new []pointer
# every constant of the program's own files by name (as written), sorted; the first declared first
var g_pmc_all: []pointer = new []pointer
var g_pmc_node: []Node = new []Node
function pmc_index() -> void {
let xs = new []int
let keys = new []pointer
let nodes = new []Node
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_CONST and d.s != null and d.file != null and not is_runtime_file(d.file) {
push(xs, len(nodes))
push(keys, vis_plain(d.s))
push(nodes, d)
}
i += 1
}
sc_sort(xs, keys)
g_pmc_all = new []pointer
g_pmc_node = new []Node
i = 0
while i < len(xs) {
push(g_pmc_all, keys[xs[i]])
push(g_pmc_node, nodes[xs[i]])
i += 1
}
}
# the const declared as name (as written), or null
function pmc_find(name: pointer) -> Node {
var lo = 0
var hi = len(g_pmc_all) - 1
var found = -1
while lo <= hi {
let mid = (lo + hi) / 2
let m = g_pmc_all[mid]
if (m == name) {
found = mid
hi = mid - 1
} else if reg_str_less(name, m) { hi = mid - 1 } else { lo = mid + 1 }
}
if found < 0 { return null }
return g_pmc_node[found]
}
# an int expression's value into g_pmc_ival; false when it is not one this can work out
function pmc_int(e: Node, depth: int) -> bool {
if e == null or depth > 24 { return false }
if e.kind == E_INT {
g_pmc_ival = e.ival
return true
}
if e.kind == E_UN and (e.s == "-") {
if not pmc_int(e.a, depth + 1) { return false }
g_pmc_ival = 0 - g_pmc_ival
return true
}
if e.kind == E_UN and (e.s == "~") {
if not pmc_int(e.a, depth + 1) { return false }
g_pmc_ival = ~g_pmc_ival
return true
}
if e.kind == E_ID {
let d = pmc_find(e.s)
if d == null or not (d.ty == "int") { return false }
return pmc_int(d.a, depth + 1)
}
if e.kind == E_BIN {
if not pmc_int(e.a, depth + 1) { return false }
let a = g_pmc_ival
if not pmc_int(e.b, depth + 1) { return false }
let b = g_pmc_ival
let op = e.s
if (op == "+") { g_pmc_ival = a + b; return true }
if (op == "-") { g_pmc_ival = a - b; return true }
if (op == "*") { g_pmc_ival = a * b; return true }
if (op == "/") and b != 0 { g_pmc_ival = a / b; return true }
if (op == "%") and b != 0 { g_pmc_ival = a % b; return true }
if (op == "<<") { g_pmc_ival = a << b; return true }
if (op == ">>") { g_pmc_ival = a >> b; return true }
if (op == "&") { g_pmc_ival = a & b; return true }
if (op == "|") { g_pmc_ival = a | b; return true }
if (op == "^") { g_pmc_ival = a ^ b; return true }
}
return false
}
# a float constant's value as text (strtod reads it at run time): a literal, maybe negated, or
# another float constant
function pmc_float(e: Node, depth: int) -> bool {
if e == null or depth > 24 { return false }
if e.kind == E_FLOAT and e.s != null {
g_pmc_fval = e.s
return true
}
if e.kind == E_INT {
g_pmc_fval = itoa(e.ival)
return true
}
if e.kind == E_UN and (e.s == "-") {
if not pmc_float(e.a, depth + 1) { return false }
if g_pmc_fval[0] == '-' { g_pmc_fval = g_pmc_fval[1 .. len(g_pmc_fval)] } else { g_pmc_fval = "-" + g_pmc_fval }
return true
}
if e.kind == E_ID {
let d = pmc_find(e.s)
if d == null or not (d.ty == "float") { return false }
return pmc_float(d.a, depth + 1)
}
return false
}
# every constant a map may name, sorted, first of each name kept
function pmc_collect() -> void {
pmc_index()
g_pmc_names = new []pointer
g_pmc_isf = new []bool
g_pmc_iv = new []int
g_pmc_ftext = new []pointer
var i = 0
while i < len(g_pmc_all) {
let d = g_pmc_node[i]
let nm = g_pmc_all[i]
let first = i == 0 or not (g_pmc_all[i - 1] == nm)
if first and (d.ty == "int") and pmc_int(d.a, 0) {
push(g_pmc_names, nm)
push(g_pmc_isf, false)
push(g_pmc_iv, g_pmc_ival)
push(g_pmc_ftext, "")
} else if first and (d.ty == "float") and pmc_float(d.a, 0) {
push(g_pmc_names, nm)
push(g_pmc_isf, true)
push(g_pmc_iv, 0)
push(g_pmc_ftext, g_pmc_fval)
}
i += 1
}
}
# the index of name in the sorted table, or -1
function pmc_lookup(name: pointer) -> int {
var lo = 0
var hi = len(g_pmc_names) - 1
while lo <= hi {
let mid = (lo + hi) / 2
let m = g_pmc_names[mid]
if (m == name) { return mid }
if reg_str_less(name, m) { hi = mid - 1 } else { lo = mid + 1 }
}
return -1
}
# lres_consts__(), in the runtime's name space
function pm_consts_gen() -> void {
pmc_collect()
let b = buf_new()
buf_puts(b, "function lres_consts__() -> string {\n return \"")
var i = 0
while i < len(g_pmc_names) {
buf_puts(b, g_pmc_names[i])
if g_pmc_isf[i] {
buf_puts(b, " f ")
buf_puts(b, g_pmc_ftext[i])
} else {
buf_puts(b, " i ")
buf_puts(b, itoa(g_pmc_iv[i]))
}
buf_puts(b, ";")
i += 1
}
buf_puts(b, "\"\n}\n")
pm_parse_src(buf_str(b), "runtime/native/lres_consts.ludic", 1, 0)
}

View file

@ -0,0 +1,481 @@
# permap_gen.ludic — what a @PerMap registry becomes: Ludic, written here and read like the rest
# (as reg_find_fn writes a registry's find), in the registry's file and module, exported with it.
#
# For `@PerMap registry Props of PropRow from "props.lres"`:
# state Props { rows: []PropRow, map: string, err: string, ... } the table, one per program
# props_load(st: mut Props, map: string) -> bool <root>/<map>/props.lres
# props_clear(st: mut Props), props_find(st: Props, key: string) -> int, props_path(st: Props, rel: string) -> string
# For `@PerMap @Chunked(64) registry Instances of InstRow from "instances/{cx}_{cz}.lres"`:
# property InstancesChunk { cx: int, cz: int, on: bool, rows: []InstRow, ... }
# state Instances { chunks: []InstancesChunk, map: string, err: string, ... }
# instances_in(st, map, cx, cz) -> int, instances_out(st, cx, cz), instances_slot(st, cx, cz) -> int,
# instances_find(st, slot, key) -> int, instances_clear(st), instances_path(st, rel) -> string
#
# THE TABLE OWNS ITS ROWS, and everything in them. A pool (R__Pool, one per table or chunk slot)
# keeps every row record the table ever made, every record a row's list ever took (by type, handed
# out again from the start on each load), and a template of each record type made once with its
# defaults. A load rewinds the pool, clears `rows` and each row's lists in place and refills them;
# a record is made only when a load needs more of that type than any load before it. So nothing
# may keep a row, a row's list or a chunk's rows across a load or an _out - keep the key or the
# index. A string is interned, so a string field is safe to keep.
# parse generated source as declarations of `file` at `line`, each exported when vis is 1
# (LUDIC_PERMAP_SRC=<file>: what was written, appended there - to read what a table became)
function pm_parse_src(src: pointer, file: pointer, line: int, vis: int) -> void {
let dump = getenv("LUDIC_PERMAP_SRC")
if dump != null {
let df = file_open(dump, "ab")
if df != null {
file_write(df, src, len(src))
file_close(df)
}
}
let saved_toks = toks
let saved_pi = pi
let saved_file = g_parse_file
let saved_parsing = g_parsing
g_parse_file = file
g_parsing = true
lex_at(src, line)
pi = 0
let at = len(prog)
while true {
skipnl()
if toks[pi].kind == TK_EOF { break }
parse_one_decl()
}
var k = at
while k < len(prog) {
prog[k].vis = vis
k += 1
}
toks = saved_toks
pi = saved_pi
g_parse_file = saved_file
g_parsing = saved_parsing
}
function pm_l(b: Buf, s: pointer) -> void {
buf_puts(b, s)
buf_puts(b, "\n")
}
# a string literal of s (a path, a type), quotes and escapes included
function pm_q(s: pointer) -> pointer {
let b = buf_new()
buf_putc(b, '"')
var i = 0
while i < len(s) {
let c = s[i]
if c == '"' or c == 92 { buf_putc(b, 92) }
buf_putc(b, c)
i += 1
}
buf_putc(b, '"')
return buf_str(b)
}
function pm_gen(r: int) -> void {
let v = g_rg_var[r]
let R = g_rg_name[r]
let sn = pm_snake(R)
let chunked = g_rg_chunk[r] > 0
let b = buf_new()
pm_gen_pool(b, R)
if chunked { pm_gen_chunk_types(b, R, g_rg_type[r]) } else { pm_gen_state(b, R, g_rg_type[r]) }
pm_gen_records(b, sn, R)
pm_gen_fns(b, sn, module_of(v.file))
pm_gen_rows(b, sn, R, g_rg_type[r])
if chunked { pm_gen_chunk_verbs(b, r, sn, R) } else { pm_gen_whole_verbs(b, r, sn, R) }
pm_l(b, `@alloc_ok("a path handed to a caller at load time - for an asset a row names - never in a frame")`)
pm_l(b, `function {sn}_path(st: {R}, rel: string) -> string {{`)
pm_l(b, ` return {pm_q(g_pm_root + "/")} + st.map + "/" + rel`)
pm_l(b, "}")
pm_parse_src(buf_str(b), v.file, v.line, v.vis)
}
# the pool: every row and every list's record the table ever made, and each type's template
function pm_gen_pool(b: Buf, R: pointer) -> void {
pm_l(b, `property {R}__Pool {{`)
pm_l(b, ` rows: []{g_pm_recs[0]} = new []{g_pm_recs[0]}`)
var i = 0
while i < len(g_pm_recs) {
let P = g_pm_recs[i]
pm_l(b, ` t_{P}: {P} = new {P}`)
if g_pm_pooled[i] {
pm_l(b, ` p_{P}: []{P} = new []{P}`)
pm_l(b, ` c_{P}: int = 0`)
}
i += 1
}
pm_l(b, "}")
}
function pm_gen_state(b: Buf, R: pointer, T: pointer) -> void {
pm_l(b, `state {R} {{`)
pm_l(b, ` rows: []{T} = new []{T}`)
pm_l(b, ` map: string = ""`)
pm_l(b, ` err: string = ""`)
pm_l(b, ` lt__: LresTree = new LresTree`)
pm_l(b, ` pl__: {R}__Pool = new {R}__Pool`)
pm_l(b, ` hs__: LresHash = new LresHash`)
pm_l(b, "}")
}
function pm_gen_chunk_types(b: Buf, R: pointer, T: pointer) -> void {
pm_l(b, `property {R}Chunk {{`)
pm_l(b, ` cx: int = 0`)
pm_l(b, ` cz: int = 0`)
pm_l(b, ` on: bool = false`)
pm_l(b, ` rows: []{T} = new []{T}`)
pm_l(b, ` pl__: {R}__Pool = new {R}__Pool`)
pm_l(b, ` hs__: LresHash = new LresHash`)
pm_l(b, "}")
pm_l(b, `state {R} {{`)
pm_l(b, ` chunks: []{R}Chunk = new []{R}Chunk`)
pm_l(b, ` map: string = ""`)
pm_l(b, ` err: string = ""`)
pm_l(b, ` lt__: LresTree = new LresTree`)
pm_l(b, "}")
}
# per record type: take one from the pool, reset one to its defaults, fill one from a node
function pm_gen_records(b: Buf, sn: pointer, R: pointer) -> void {
pm_l(b, `function {sn}__rewind(pl: {R}__Pool) -> void {{`)
var i = 0
while i < len(g_pm_recs) {
if g_pm_pooled[i] { pm_l(b, ` pl.c_{g_pm_recs[i]} = 0`) }
i += 1
}
pm_l(b, "}")
i = 0
while i < len(g_pm_recs) {
let P = g_pm_recs[i]
if g_pm_pooled[i] {
pm_l(b, `@alloc_ok("a {P} past the most a load of this table has used: made once, kept in its pool (high water)")`)
pm_l(b, `function {sn}__take_{P}(pl: {R}__Pool) -> {P} {{`)
pm_l(b, ` if pl.c_{P} < len(pl.p_{P}) {{`)
pm_l(b, ` let r = pl.p_{P}[pl.c_{P}]`)
pm_l(b, ` pl.c_{P} += 1`)
pm_l(b, " return r")
pm_l(b, " }")
pm_l(b, ` let r = new {P}`)
pm_l(b, ` push(pl.p_{P}, r)`)
pm_l(b, ` pl.c_{P} += 1`)
pm_l(b, " return r")
pm_l(b, "}")
}
pm_gen_reset(b, sn, R, P)
pm_gen_fill(b, sn, R, P)
i += 1
}
}
# every field back to the template's value; a list emptied in place (its scalars from the template
# again), a nested record reset in place - never the template's own list or record, which would alias
function pm_gen_reset(b: Buf, sn: pointer, R: pointer, P: pointer) -> void {
let c = find_comp(P)
pm_l(b, `@alloc_ok("a record's own list or record, made the first time the record is reset (high water)")`)
pm_l(b, `function {sn}__reset_{P}(pl: {R}__Pool, r: {P}) -> void {{`)
pm_l(b, ` let d = pl.t_{P}`)
var i = 0
while i < len(c.kids) {
let f = c.kids[i]
let k = pm_kind(f.ty)
let x = f.s
if k == PM_LIST {
pm_l(b, ` if r.{x} == null {{ r.{x} = new {f.ty} }} else {{ List.clear(r.{x}) }}`)
if pm_kind(slice_elem(f.ty)) != PM_REC {
pm_l(b, ` if d.{x} != null {{`)
pm_l(b, ` var i{itoa(i)} = 0`)
pm_l(b, ` while i{itoa(i)} < len(d.{x}) {{`)
pm_l(b, ` push(r.{x}, d.{x}[i{itoa(i)}])`)
pm_l(b, ` i{itoa(i)} += 1`)
pm_l(b, " }")
pm_l(b, " }")
}
} else if k == PM_REC {
pm_l(b, ` if r.{x} == null {{ r.{x} = new {f.ty} }}`)
pm_l(b, ` {sn}__reset_{f.ty}(pl, r.{x})`)
} else {
pm_l(b, ` r.{x} = d.{x}`)
}
i += 1
}
pm_l(b, "}")
}
# a record node's fields into r; a wrong one is the reader's error (lres_ok says so)
function pm_gen_fill(b: Buf, sn: pointer, R: pointer, P: pointer) -> void {
let c = find_comp(P)
pm_l(b, `@alloc_ok("a row's list grows only past the most it has held (high water)")`)
pm_l(b, `function {sn}__fill_{P}(t: LresTree, pl: {R}__Pool, r: {P}, n: int) -> void {{`)
pm_l(b, " var c = lres_kid(t, n)")
pm_l(b, " while c >= 0 and lres_ok(t) {")
pm_l(b, " let f = lres_name(t, c)")
var i = 0
while i < len(c.kids) {
let fd = c.kids[i]
var els = "else "
if i == 0 { els = "" }
pm_l(b, ` {els}if f == {pm_q(fd.s)} {{`)
pm_gen_field(b, sn, fd, i)
pm_l(b, " }")
i += 1
}
if len(c.kids) > 0 { pm_l(b, ` else {{ lres_no_field(t, c, {pm_q(P)}) }}`) } else { pm_l(b, ` lres_no_field(t, c, {pm_q(P)})`) }
pm_l(b, " c = lres_next(t, c)")
pm_l(b, " }")
pm_l(b, "}")
}
function pm_scalar_read(k: int) -> pointer {
if k == PM_INT { return "lres_int" }
if k == PM_FLOAT { return "lres_float" }
if k == PM_BOOL { return "lres_bool" }
return "lres_str"
}
function pm_gen_field(b: Buf, sn: pointer, fd: Node, i: int) -> void {
let k = pm_kind(fd.ty)
let x = fd.s
let n = itoa(i)
if k == PM_INT or k == PM_FLOAT or k == PM_BOOL or k == PM_STR {
pm_l(b, ` r.{x} = {pm_scalar_read(k)}(t, c)`)
return
}
if k == PM_FN {
pm_l(b, ` r.{x} = {sn}__fn_{itoa(pm_fn_index(fd.ty))}(t, c, r.{x})`)
return
}
if k == PM_REC {
pm_l(b, ` if lres_is_rec(t, c) {{ {sn}__fill_{fd.ty}(t, pl, r.{x}, c) }}`)
return
}
let el = slice_elem(fd.ty)
let ek = pm_kind(el)
pm_l(b, " if lres_is_list(t, c) {")
pm_l(b, ` List.clear(r.{x})`)
pm_l(b, ` var k{n} = lres_kid(t, c)`)
pm_l(b, ` while k{n} >= 0 {{`)
if ek == PM_REC {
pm_l(b, ` let e{n} = {sn}__take_{el}(pl)`)
pm_l(b, ` {sn}__reset_{el}(pl, e{n})`)
pm_l(b, ` if lres_is_rec(t, k{n}) {{ {sn}__fill_{el}(t, pl, e{n}, k{n}) }}`)
pm_l(b, ` push(r.{x}, e{n})`)
} else {
pm_l(b, ` push(r.{x}, {pm_scalar_read(ek)}(t, k{n}))`)
}
pm_l(b, ` k{n} = lres_next(t, k{n})`)
pm_l(b, " }")
pm_l(b, " }")
}
# per fn type a field holds: `fn name` -> the function, among the program's functions of that type
# the registry's module can see (the schema's fn_type)
function pm_gen_fns(b: Buf, sn: pointer, mod: pointer) -> void {
var i = 0
while i < len(g_pm_fnty) {
let ft = g_pm_fnty[i]
pm_l(b, `function {sn}__fn_{itoa(i)}(t: LresTree, c: int, cur: {ft}) -> {ft} {{`)
pm_l(b, " let nm = lres_fn(t, c)")
let cands = pm_fn_cands(ft, mod)
var j = 0
while j < len(cands) {
let nm = vis_plain(cands[j].s)
pm_l(b, ` if nm == {pm_q(nm)} {{ return fn {nm} }}`)
j += 1
}
pm_l(b, ` if len(nm) > 0 {{ lres_no_fn(t, c, {pm_q(ft)}) }}`)
pm_l(b, " return cur")
pm_l(b, "}")
i += 1
}
}
# a function's type as a fn value of it has: its parameters past the states, its result
function pm_fn_type(d: Node) -> pointer {
var args: pointer = ""
var k = 0
while k < len(d.kids) {
let p = d.kids[k]
if p.kind == N_PARAM and not (is_state_ty(p.ty) or is_state_ty(vis_plain(p.ty))) {
if len(args) > 0 { args = args + "," }
args = args + p.ty
}
k += 1
}
var ret: pointer = "void"
if d.ty != null { ret = d.ty }
return "fn(" + args + ")->" + ret
}
# the functions of type ft that module mod can name: its own, the runtime's, another's exported one
# it `uses` - written in the program, not generic
function pm_fn_cands(ft: pointer, mod: pointer) -> []Node {
let out = new []Node
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_FN and d.s != null and d.tps == null and d.pos >= 0 and d.file != null and not is_runtime_file(d.file) {
if (pm_fn_type(d) == ft) and pm_fn_visible(d, mod) { push(out, d) }
}
i += 1
}
return out
}
function pm_fn_visible(d: Node, mod: pointer) -> bool {
let to = module_of(d.file)
if not module_may_use(mod, module_for_uses(d.file)) { return false }
if (to == "") or (to == mod) { return true }
return d.vis == 1 or module_friend_of(mod, to)
}
# the rows of one file: each entry a row, reset, filled and keyed; the keys hashed as they come
function pm_gen_rows(b: Buf, sn: pointer, R: pointer, T: pointer) -> void {
pm_l(b, `@alloc_ok("a row past the most this table has held: made once, kept in its pool (high water)")`)
pm_l(b, `function {sn}__row(pl: {R}__Pool, i: int) -> {T} {{`)
pm_l(b, " if i < len(pl.rows) { return pl.rows[i] }")
pm_l(b, ` let r = new {T}`)
pm_l(b, " push(pl.rows, r)")
pm_l(b, " return r")
pm_l(b, "}")
pm_l(b, `@alloc_ok("the rows list grows only past the most rows this table has held (high water)")`)
pm_l(b, `function {sn}__fill_rows(t: LresTree, pl: {R}__Pool, rows: []{T}, hs: LresHash) -> bool {{`)
pm_l(b, " List.clear(rows)")
pm_l(b, ` {sn}__rewind(pl)`)
pm_l(b, " lres_hash_reset(hs, lres_entries(t))")
pm_l(b, " var e = lres_top(t)")
pm_l(b, " var i = 0")
pm_l(b, " while e >= 0 and lres_ok(t) {")
pm_l(b, ` let r = {sn}__row(pl, i)`)
pm_l(b, ` {sn}__reset_{T}(pl, r)`)
pm_l(b, ` {sn}__fill_{T}(t, pl, r, e)`)
pm_l(b, " r.key = lres_name(t, e)")
pm_l(b, " if lres_hash_put(hs, r.key, i) >= 0 { lres_dup(t, e) }")
pm_l(b, " push(rows, r)")
pm_l(b, " i += 1")
pm_l(b, " e = lres_next(t, e)")
pm_l(b, " }")
pm_l(b, " if lres_ok(t) { return true }")
pm_l(b, ` {sn}__drop(pl, rows, hs)`)
pm_l(b, " return false")
pm_l(b, "}")
pm_l(b, `function {sn}__drop(pl: {R}__Pool, rows: []{T}, hs: LresHash) -> void {{`)
pm_l(b, " List.clear(rows)")
pm_l(b, ` {sn}__rewind(pl)`)
pm_l(b, " lres_hash_reset(hs, 0)")
pm_l(b, "}")
}
# `<root>/<map>/` and the file, with {cx} / {cz} written as the chunk's numbers, into the reader's path
function pm_gen_path(b: Buf, r: int) -> void {
pm_l(b, ` lres_path_begin(t, {pm_q(g_pm_root + "/")})`)
pm_l(b, " lres_path_add(t, map)")
pm_l(b, ` lres_path_add(t, "/")`)
let from = g_rg_from[r]
var a = 0
var i = 0
while i < len(from) {
if from[i] == '{' and i + 3 < len(from) and from[i + 1] == 'c' and (from[i + 2] == 'x' or from[i + 2] == 'z') and from[i + 3] == '}' {
if i > a { pm_l(b, ` lres_path_add(t, {pm_q(from[a .. i])})`) }
if from[i + 2] == 'x' { pm_l(b, " lres_path_int(t, cx)") } else { pm_l(b, " lres_path_int(t, cz)") }
i += 4
a = i
} else {
i += 1
}
}
if len(from) > a { pm_l(b, ` lres_path_add(t, {pm_q(from[a .. len(from)])})`) }
}
function pm_gen_whole_verbs(b: Buf, r: int, sn: pointer, R: pointer) -> void {
pm_l(b, `@alloc_ok("a load grows the table's rows and lists only past its high water; an error's message only when a file is wrong")`)
pm_l(b, `function {sn}_load(st: mut {R}, map: string) -> bool {{`)
pm_l(b, " let t = st.lt__")
pm_gen_path(b, r)
pm_l(b, " let got = lres_read(t)")
pm_l(b, " if got == 0 { lres_missing(t) }")
pm_l(b, ` if got <= 0 or not {sn}__fill_rows(t, st.pl__, st.rows, st.hs__) {{`)
pm_l(b, ` {sn}__drop(st.pl__, st.rows, st.hs__)`)
pm_l(b, " st.err = lres_error(t)")
pm_l(b, " st.map = \"\"")
pm_l(b, " return false")
pm_l(b, " }")
pm_l(b, " st.map = intern(map)")
pm_l(b, " st.err = \"\"")
pm_l(b, " return true")
pm_l(b, "}")
pm_l(b, `function {sn}_clear(st: mut {R}) -> void {{`)
pm_l(b, ` {sn}__drop(st.pl__, st.rows, st.hs__)`)
pm_l(b, " st.map = \"\"")
pm_l(b, " st.err = \"\"")
pm_l(b, "}")
pm_l(b, `function {sn}_find(st: {R}, key: string) -> int {{`)
pm_l(b, " return lres_hash_get(st.hs__, key)")
pm_l(b, "}")
}
function pm_gen_chunk_verbs(b: Buf, r: int, sn: pointer, R: pointer) -> void {
pm_l(b, `function {sn}__off(ch: {R}Chunk) -> void {{`)
pm_l(b, " ch.on = false")
pm_l(b, ` {sn}__drop(ch.pl__, ch.rows, ch.hs__)`)
pm_l(b, "}")
pm_l(b, `function {sn}_slot(st: {R}, cx: int, cz: int) -> int {{`)
pm_l(b, " var i = 0")
pm_l(b, " while i < len(st.chunks) {")
pm_l(b, " let ch = st.chunks[i]")
pm_l(b, " if ch.on and ch.cx == cx and ch.cz == cz { return i }")
pm_l(b, " i += 1")
pm_l(b, " }")
pm_l(b, " return -1")
pm_l(b, "}")
pm_l(b, `@alloc_ok("a chunk slot past the most chunks this table has held at once, and rows past a slot's high water: made once and kept; an error's message only when a file is wrong")`)
pm_l(b, `function {sn}_in(st: mut {R}, map: string, cx: int, cz: int) -> int {{`)
pm_l(b, " if not (st.map == map) {")
pm_l(b, " var j = 0")
pm_l(b, " while j < len(st.chunks) {")
pm_l(b, ` {sn}__off(st.chunks[j])`)
pm_l(b, " j += 1")
pm_l(b, " }")
pm_l(b, " st.map = intern(map)")
pm_l(b, " }")
pm_l(b, ` let have = {sn}_slot(st, cx, cz)`)
pm_l(b, " if have >= 0 { return have }")
pm_l(b, " var s = -1")
pm_l(b, " var i = 0")
pm_l(b, " while i < len(st.chunks) and s < 0 {")
pm_l(b, " if not st.chunks[i].on { s = i }")
pm_l(b, " i += 1")
pm_l(b, " }")
pm_l(b, " if s < 0 {")
pm_l(b, ` push(st.chunks, new {R}Chunk)`)
pm_l(b, " s = len(st.chunks) - 1")
pm_l(b, " }")
pm_l(b, " let ch = st.chunks[s]")
pm_l(b, ` {sn}__off(ch)`)
pm_l(b, " ch.cx = cx")
pm_l(b, " ch.cz = cz")
pm_l(b, " let t = st.lt__")
pm_gen_path(b, r)
pm_l(b, " let got = lres_read(t)")
pm_l(b, " if got == 0 {")
pm_l(b, " ch.on = true")
pm_l(b, " st.err = \"\"")
pm_l(b, " return s")
pm_l(b, " }")
pm_l(b, ` if got < 0 or not {sn}__fill_rows(t, ch.pl__, ch.rows, ch.hs__) {{`)
pm_l(b, ` {sn}__off(ch)`)
pm_l(b, " st.err = lres_error(t)")
pm_l(b, " return -1")
pm_l(b, " }")
pm_l(b, " ch.on = true")
pm_l(b, " st.err = \"\"")
pm_l(b, " return s")
pm_l(b, "}")
pm_l(b, `function {sn}_out(st: mut {R}, cx: int, cz: int) -> void {{`)
pm_l(b, ` let s = {sn}_slot(st, cx, cz)`)
pm_l(b, ` if s >= 0 {{ {sn}__off(st.chunks[s]) }}`)
pm_l(b, "}")
pm_l(b, `function {sn}_clear(st: mut {R}) -> void {{`)
pm_l(b, " var i = 0")
pm_l(b, " while i < len(st.chunks) {")
pm_l(b, ` {sn}__off(st.chunks[i])`)
pm_l(b, " i += 1")
pm_l(b, " }")
pm_l(b, " st.map = \"\"")
pm_l(b, " st.err = \"\"")
pm_l(b, "}")
pm_l(b, `function {sn}_find(st: {R}, slot: int, key: string) -> int {{`)
pm_l(b, " if slot < 0 or slot >= len(st.chunks) { return -1 }")
pm_l(b, " if not st.chunks[slot].on { return -1 }")
pm_l(b, " return lres_hash_get(st.chunks[slot].hs__, key)")
pm_l(b, "}")
}

View file

@ -64,6 +64,8 @@ function reg_lower(s: pointer) -> pointer {
}
# registry NAME of TYPE [as PREFIX]
function parse_registry() -> void {
let pm = g_pm_pending # @PerMap / @Chunked(n) (permap.ludic)
let chunk = g_pm_chunk_pending
pi += 1
let v = node(N_VAR)
v.s = eat_id()
@ -72,6 +74,7 @@ function parse_registry() -> void {
let t = ptype()
var prefix = reg_upper(v.s)
if is_id("as") {
if pm { perr(`registry {v.s}: a @PerMap registry has no 'as PREFIX' - its keys are not known when the game compiles, so it has no constants; find a row by its key with {pm_snake(v.s)}_find`) }
pi += 1
prefix = eat_id()
}
@ -82,7 +85,7 @@ function parse_registry() -> void {
pi += 1
}
v.ty = "[]" + t
push(prog, v)
if not pm { push(prog, v) } # a map-scoped table is a state, written when the program is read
push(g_rg_name, v.s)
push(g_rg_type, t)
push(g_rg_prefix, prefix)
@ -90,7 +93,8 @@ function parse_registry() -> void {
push(g_rg_open, 0)
push(g_rg_from, from)
push(g_rg_at, at_registry_take()) # attrs.ludic
if from != null { res_read(v, from) }
pm_declared(pm, chunk, v.s)
if from != null and not pm { res_read(v, from) }
}
# def REGISTRY key { field: value, ... }
# def REGISTRY from "file.lres" - every entry of a resource file, as defs of this file's module

View file

@ -8,14 +8,16 @@ function registries_finish() -> void {
g_parsing = false
perr(`def {g_df_reg[d]} {g_df_key[d]}: there is no registry {g_df_reg[d]}`)
}
pm_def_check(d) # permap.ludic: a map-scoped registry takes no def
reg_def_check(d) # registry_open.ludic: open, and visible
d += 1
}
var r = 0
while r < len(g_rg_name) {
reg_fill(r)
if not pm_is(r) { reg_fill(r) }
r += 1
}
pm_finish() # permap.ludic: each map-scoped registry's state and verbs
}
function reg_fill(r: int) -> void {
let v = g_rg_var[r]