@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:
parent
7170c31304
commit
2f4860ae71
14 changed files with 137400 additions and 100393 deletions
|
|
@ -43,7 +43,9 @@ function schema_buf() -> Buf {
|
|||
sc_components(b)
|
||||
buf_puts(b, "\n ],\n \"natives\": [")
|
||||
sc_natives(b)
|
||||
buf_puts(b, "\n ]\n}\n")
|
||||
buf_puts(b, "\n ],\n \"units\": [")
|
||||
sc_units(b)
|
||||
buf_puts(b, "]\n}\n")
|
||||
return b
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +191,7 @@ function sc_attrs(b: Buf, g: int) -> void {
|
|||
buf_puts(b, ", \"args\": ")
|
||||
buf_puts(b, g_at_args[j])
|
||||
if g_at_unres[j] { buf_puts(b, ", \"unresolved\": true") }
|
||||
if (g_at_name[j] == "Ref") and g_at_first[j] != null and pm_named(g_at_first[j]) { buf_puts(b, ", \"scope\": \"map\"") }
|
||||
buf_puts(b, "}")
|
||||
j += 1
|
||||
}
|
||||
|
|
@ -365,7 +368,7 @@ function sc_registry(b: Buf, r: int, first: bool) -> void {
|
|||
buf_puts(b, ", \"record\": ")
|
||||
jq_put(b, sc_ty(g_rg_type[r]))
|
||||
buf_puts(b, ", \"prefix\": ")
|
||||
jq_put(b, g_rg_prefix[r])
|
||||
if pm_is(r) { buf_puts(b, "null") } else { jq_put(b, g_rg_prefix[r]) } # a map's rows have no constants
|
||||
buf_puts(b, ", \"source\": ")
|
||||
sc_str(b, g_rg_from[r])
|
||||
buf_puts(b, ", \"open\": ")
|
||||
|
|
@ -374,6 +377,14 @@ function sc_registry(b: Buf, r: int, first: bool) -> void {
|
|||
sc_bool(b, v.vis == 1)
|
||||
buf_puts(b, ", \"module\": ")
|
||||
sc_str(b, sc_mod(v.file))
|
||||
if pm_is(r) { # permap.ludic: read per map at run time, per chunk when chunked
|
||||
buf_puts(b, ", \"scope\": \"map\", \"chunk\": ")
|
||||
if g_rg_chunk[r] > 0 { buf_puti(b, g_rg_chunk[r]) } else { buf_puts(b, "null") }
|
||||
buf_puts(b, ", \"maps\": ")
|
||||
jq_put(b, g_pm_root)
|
||||
} else {
|
||||
buf_puts(b, ", \"scope\": \"game\", \"chunk\": null")
|
||||
}
|
||||
buf_puts(b, ",\n ")
|
||||
sc_at(b, v.file, v.line, v.col)
|
||||
buf_puts(b, ", \"attributes\": ")
|
||||
|
|
@ -605,3 +616,14 @@ function sc_function(b: Buf, d: Node, first: bool) -> void {
|
|||
sc_str(b, sc_doc(d.file, d.pos))
|
||||
buf_puts(b, "}")
|
||||
}
|
||||
|
||||
# @Unit's canonical spellings (attrs.ludic): what an editor offers and a converter reads
|
||||
function sc_units(b: Buf) -> void {
|
||||
let us = at_units()
|
||||
var i = 0
|
||||
while i < len(us) {
|
||||
if i > 0 { buf_puts(b, ", ") }
|
||||
jq_put(b, us[i])
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
309
selfhost/frontend/permap.ludic
Normal file
309
selfhost/frontend/permap.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
549
selfhost/frontend/permap_check.ludic
Normal file
549
selfhost/frontend/permap_check.ludic
Normal 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]
|
||||
}
|
||||
182
selfhost/frontend/permap_consts.ludic
Normal file
182
selfhost/frontend/permap_consts.ludic
Normal 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)
|
||||
}
|
||||
481
selfhost/frontend/permap_gen.ludic
Normal file
481
selfhost/frontend/permap_gen.ludic
Normal 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, "}")
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
114387
selfhost/ludicc.seed.ll
114387
selfhost/ludicc.seed.ll
File diff suppressed because one or more lines are too long
121736
selfhost/ludicc.win.seed.ll
121736
selfhost/ludicc.win.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -158,6 +158,7 @@ entry {
|
|||
else if a == "--save-temps" { save = true }
|
||||
else if a == "--run" { run = true }
|
||||
else if a == "--check" { g_check_only = true }
|
||||
else if a == "--no-maps" { g_pm_no_maps = true } # --check leaves the map directories alone (permap_check.ludic)
|
||||
else if a == "--release" { g_release = true } # what ships: Build.schema_hash() is 0 (emit_schema_hash.ludic)
|
||||
else if a == "--diagnostics=json" { g_diag_json = true } # every error, as JSON on stdout (diag.ludic)
|
||||
else if a == "--diagnostics=text" { g_diag_json = false }
|
||||
|
|
@ -257,6 +258,7 @@ entry {
|
|||
# --check: every check a build makes - the emitter refuses things too (a bind to a function that is
|
||||
# gone, an unknown name) - and nothing written
|
||||
if g_check_only {
|
||||
pm_check_maps() # every map's tables against their records (permap_check.ludic)
|
||||
emit_program()
|
||||
if g_dg_errors > 0 { diag_exit(1) }
|
||||
diag_exit(0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue