ludic/selfhost/frontend/permap_check.ludic

635 lines
19 KiB
Text

# 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
var g_pmk_md: pointer = "" # the map directory 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_md = md
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()
pmk_chunk_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()
pmk_game_assets()
i18n_permap_rows(md) # i18n.ludic: its text keys and English left
}
# 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
}
}
# a chunked table's key is unique across its map (a row's id is (map, key), and a tree moved into
# another chunk keeps it): a key in two of a map's chunk files is refused, naming both
function pmk_chunk_dups() -> void {
let xs = new []int
let keys = new []pointer
var i = 0
while i < len(g_pmk_key) {
if g_rg_chunk[g_pmk_reg[i]] > 0 {
push(xs, i)
push(keys, `{g_rg_name[g_pmk_reg[i]]}\n{g_pmk_key[i]}`)
} else { push(keys, "") }
i += 1
}
sc_sort(xs, keys)
i = 1
while i < len(xs) {
let a = xs[i - 1]
let k = xs[i]
if (keys[k] == keys[a]) and not (g_pmk_file[k] == g_pmk_file[a]) {
pmk_err(g_pmk_file[k], g_pmk_kline[k], g_pmk_kcol[k], `the key {g_pmk_key[k]} is also written at {g_pmk_file[a]}:{itoa(g_pmk_kline[a])} - a chunked table's key is unique across its map`)
}
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 i18n_is_key(e) { return "a key" }
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 and i18n_is_key_ty(ty) {
if not i18n_is_key(e) { pmk_want(e, f, "a key k\"...\"") }
return
}
if k == PM_STR {
if e.kind != E_STR or i18n_is_key(e) { 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
}
# ---- map-relative assets: @Asset("png", map) -----------------------------------------------------
function pmk_exists(p: pointer) -> bool {
let fh = file_open(p, "rb")
if fh == null { return false }
file_close(fh)
return true
}
# a value of an @Asset(kind, map) field, at node at: the file is in this map's directory, unless optional
function pmk_asset(at: Node, f: Node, v: pointer) -> void {
let m = at_asset_map(f)
if m != 1 { return }
let p = `{g_pmk_md}/{v}`
if not pmk_exists(p) { pmk_err_at(at, `{f.s} is "{v}", and this map has no {p} - @Asset(..., map) is a path under each map's directory`) }
}
# a game-wide registry's rows whose @Asset(kind, map) field names a file: that file in every map
function pmk_game_assets() -> void {
var r = 0
while r < len(g_rg_name) {
if not pm_is(r) { pmk_game_asset_reg(r) }
r += 1
}
}
function pmk_game_asset_reg(r: int) -> void {
let comp = find_comp(g_rg_type[r])
if comp == null { return }
var d = 0
while d < len(g_df_rec) {
if (g_df_reg[d] == g_rg_name[r]) { pmk_game_asset_row(comp, g_df_rec[d]) }
d += 1
}
}
function pmk_game_asset_row(comp: Node, rec: Node) -> void {
var k = 0
while k < len(rec.kids) {
let fi = rec.kids[k]
if fi.kind == E_FINIT and fi.a != null and fi.a.kind == E_STR and len(fi.a.s) > 0 {
let f = pmk_field_of(comp, fi.s)
if f != null { pmk_asset(fi, f, fi.a.s) }
}
k += 1
}
}
function pmk_field_of(comp: Node, name: pointer) -> Node {
var i = 0
while i < len(comp.kids) {
if comp.kids[i].s != null and (comp.kids[i].s == name) { return comp.kids[i] }
i += 1
}
return null
}
# ---- 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 == "Asset") and len(e.s) > 0 { pmk_asset(e, f, e.s) }
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]
}