schema: ludicc --emit-schema / ludic schema, --check --diagnostics=json, and editor attributes

--emit-schema FILE writes the compiler's resolved view once the program type-checks: every
record (fields, types, defaults as written, docs, places, attributes), every registry with its
entries in their final order after the open-registry merge (key, constant, index, file:line:col
of the entry and of each field value, and which file contributed which keys), every const, and
the zero-argument functions a fn value can name. Deterministic, schema_version 1; the runtime is
left out. `ludic schema [file] [-o FILE]` wraps it.

--check --diagnostics=json prints every error as one JSON array on stdout: the checker's and the
module rules' all, a parse or lowering error as the last. Tokens and nodes now carry a column.

Fields take several @attributes; @Ref(Registry), @OneOf(PREFIX_), @Range(lo, hi), @Unit("..."),
@Asset("..."), @Color on a field and @AppendOnly / @ByKey on a registry change nothing but go into
the schema, and @Ref naming no registry is an error (every one reported). Fixtures:
examples/lang/attributes.ludic, examples/rejected/ref_unknown.ludic, cases in ludic-dev test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 18:06:54 +03:00
parent f8d811aa97
commit 42deb76c28
27 changed files with 1220 additions and 23 deletions

View file

@ -135,6 +135,10 @@ function fp_promote_note(from: pointer, t: pointer) -> void {
if g_warn_promote == 0 { return }
var file = g_err_file
if (file == null) { file = "" }
if g_diag_json {
diag_add(file, g_err_line, 0, "warning", `a computed {from} is promoted to {t}`)
return
}
let m = `{file}:{itoa(g_err_line)}: warning: a computed {from} is promoted to {t}\n`
file_write(file_stderr(), m, len(m))
}

View file

@ -261,8 +261,12 @@ function deps_keeps(f: pointer, seen: []bool) -> void {
if kind == null { kind = "text" }
deps_line(f, `fkeep {kind} {d.file}:{itoa(site.line)} {at} {fr_chain(k)}`)
if g_arena_strict {
let m = `{d.file}:{itoa(site.line)}: error: frame code keeps what it makes: this {kind} is kept at {at} ({fr_chain(k)}) - keep(...) or intern(...) to copy it on purpose, or @alloc_ok("why")\n`
file_write(file_stderr(), m, len(m))
let fm = `frame code keeps what it makes: this {kind} is kept at {at} ({fr_chain(k)}) - keep(...) or intern(...) to copy it on purpose, or @alloc_ok("why")`
if g_diag_json { diag_add(d.file, site.line, site.col, "error", fm) }
else {
let m = `{d.file}:{itoa(site.line)}: error: {fm}\n`
file_write(file_stderr(), m, len(m))
}
}
n += 1
}
@ -270,7 +274,7 @@ function deps_keeps(f: pointer, seen: []bool) -> void {
s += 1
}
deps_line(f, `frame_keeps {itoa(n)}`)
if g_arena_strict and n > 0 { exit(1) }
if g_arena_strict and n > 0 { diag_exit(1) }
deps_births(f, seen)
deps_resources(f) # 25.5e
deps_owned(f)

View file

@ -0,0 +1,589 @@
# emit_schema.ludic — `ludicc --emit-schema FILE`: what an editor of the program's data needs, from
# the compiler's own view once the types are checked and every open registry has its entries. One
# JSON object, schema_version 1:
#
# records every property, state and event: its fields with their types, defaults, docs and
# attributes (attrs.ludic)
# registries every registry: its record, prefix, resource file, open or not, attributes, and its
# entries in their final order - key, constant, index, where each entry and each of its
# field values is written - with which file contributed which entries
# consts every const: its type and its value as written
# functions every function a `fn` value can name with no arguments (the states it takes are the
# runtime's to supply)
#
# Deterministic: each list sorted by name (then file and line), an entry list in index order, the
# paths as the compiler read them, and nothing about the run. The engine's runtime is left out.
var g_schema_out: pointer = null
function schema_write(path: pointer) -> bool {
let b = buf_new()
buf_puts(b, "{\n \"schema_version\": 1,\n \"entry\": ")
jq_put(b, g_mg_entry)
buf_puts(b, ",\n \"records\": [")
sc_records(b)
buf_puts(b, "\n ],\n \"registries\": [")
sc_registries(b)
buf_puts(b, "\n ],\n \"consts\": [")
sc_consts(b)
buf_puts(b, "\n ],\n \"functions\": [")
sc_functions(b)
buf_puts(b, "\n ]\n}\n")
let f = file_open(path, "wb")
if f == null { return false }
file_write(f, buf_str(b), b.len)
file_close(f)
return true
}
# ---- names and text ---------------------------------------------------------------------------
# a declaration's name as written: without the module a private clash renamed it for
function sc_name(s: pointer) -> pointer {
if s == null { return "" }
var i = 0
while i < len(g_rt_new) {
if (g_rt_new[i] == s) { return g_rt_old[i] }
i += 1
}
i = 0
while i < len(g_re_new) {
if (g_re_new[i] == s) { return g_re_old[i] }
i += 1
}
return vis_plain(s)
}
# a type as written: Pool<Thing> for the instance Pool$Thing, a private record by its own name
function sc_ty(t: pointer) -> pointer {
if t == null { return "void" }
var out: pointer = ""
var a = 0
var i = 0
let n = len(t)
while i <= n {
if i == n or not gen_name_ch(t[i]) {
if i > a { out = out + sc_ty_word(t[a .. i]) }
if i < n { out = out + t[i .. i + 1] }
a = i + 1
}
i += 1
}
return out
}
function sc_ty_word(w: pointer) -> pointer {
let g = gen_find(w)
if g < 0 { return sc_name(w) }
let args = gen_split(g_gi_args[g])
var out = sc_name(g_gi_gen[g]) + "<"
var i = 0
while i < len(args) {
if i > 0 { out = out + ", " }
out = out + sc_ty(args[i])
i += 1
}
return out + ">"
}
function sc_src_of(file: pointer) -> pointer {
if file == null { return null }
let s = fm_get(g_fm_src, file)
if s == null or len(s) == 0 { return null }
return s
}
# the text between two offsets of a file, or null
function sc_text(file: pointer, a: int, b: int) -> pointer {
let s = sc_src_of(file)
if s == null or a < 0 or b <= a or b > len(s) { return null }
return s[a .. b]
}
# the comment block on the lines above a declaration (past its @attribute lines), else the comment
# that ends its own line; null when it has neither
function sc_doc(file: pointer, pos: int) -> pointer {
let s = sc_src_of(file)
if s == null or pos < 0 or pos >= len(s) { return null }
var ls = pos
while ls > 0 and s[ls - 1] != '\n' { ls -= 1 }
let lines = new []pointer
var e = ls
while e > 0 {
let le = e - 1
var bgn = le
while bgn > 0 and s[bgn - 1] != '\n' { bgn -= 1 }
var k = bgn
while k < le and (s[k] == ' ' or s[k] == '\t') { k += 1 }
if k < le and s[k] == '#' {
push(lines, sc_comment(s, k, le))
e = bgn
} else if k < le and s[k] == '@' {
e = bgn
} else {
break
}
}
if len(lines) > 0 {
var out: pointer = ""
var j = len(lines) - 1
while j >= 0 {
out = out + lines[j]
if j > 0 { out = out + "\n" }
j -= 1
}
return out
}
# the comment at the end of its own line, outside any string
var i = pos
var q = 0
while i < len(s) and s[i] != '\n' {
let c = s[i]
if q != 0 {
if c == CH_BACKSLASH { i += 1 }
else if c == q { q = 0 }
}
else if c == '"' or c == '`' { q = c }
else if c == '#' {
var le2 = i
while le2 < len(s) and s[le2] != '\n' { le2 += 1 }
return sc_comment(s, i, le2)
}
i += 1
}
return null
}
# a comment's text: past its '#' and one space, without the end of its line
function sc_comment(s: pointer, hash: int, le: int) -> pointer {
var a = hash + 1
if a < le and s[a] == ' ' { a += 1 }
var z = le
while z > a and (s[z - 1] == ' ' or s[z - 1] == '\t' or s[z - 1] == '\r') { z -= 1 }
return s[a .. z]
}
function sc_str(b: Buf, s: pointer) -> void {
if s == null { buf_puts(b, "null") } else { jq_put(b, s) }
}
function sc_mod(file: pointer) -> pointer {
let m = module_of(file)
if m == null or len(m) == 0 { return null }
return m
}
function sc_bool(b: Buf, v: bool) -> void {
if v { buf_puts(b, "true") } else { buf_puts(b, "false") }
}
# a group of attributes as a JSON array
function sc_attrs(b: Buf, g: int) -> void {
buf_puts(b, "[")
if g >= 0 {
var j = g_at_start[g]
while j < g_at_start[g] + g_at_count[g] {
if j > g_at_start[g] { buf_puts(b, ", ") }
buf_puts(b, "{\"name\": ")
jq_put(b, g_at_name[j])
buf_puts(b, ", \"args\": ")
buf_puts(b, g_at_args[j])
buf_puts(b, "}")
j += 1
}
}
buf_puts(b, "]")
}
# "file", "line", "col" of a place
function sc_at(b: Buf, file: pointer, line: int, col: int) -> void {
buf_puts(b, "\"file\": ")
sc_str(b, file)
buf_puts(b, ", \"line\": ")
buf_puti(b, line)
buf_puts(b, ", \"col\": ")
buf_puti(b, col)
}
function sc_sep(b: Buf, first: bool, indent: pointer) -> void {
if not first { buf_puts(b, ",") }
buf_puts(b, "\n")
buf_puts(b, indent)
}
# ---- order ------------------------------------------------------------------------------------
# indices sorted by their keys, ties kept in the order given (a merge sort)
function sc_sort(xs: []int, keys: []pointer) -> void {
let tmp = new []int
var i = 0
while i < len(xs) { push(tmp, 0); i += 1 }
sc_msort(xs, keys, 0, len(xs), tmp)
}
function sc_msort(xs: []int, keys: []pointer, lo: int, hi: int, tmp: []int) -> void {
if hi - lo < 2 { return }
let mid = (lo + hi) / 2
sc_msort(xs, keys, lo, mid, tmp)
sc_msort(xs, keys, mid, hi, tmp)
var i = lo
var j = mid
var k = lo
while k < hi {
if j >= hi or (i < mid and not reg_str_less(keys[xs[j]], keys[xs[i]])) {
tmp[k] = xs[i]
i += 1
} else {
tmp[k] = xs[j]
j += 1
}
k += 1
}
k = lo
while k < hi {
xs[k] = tmp[k]
k += 1
}
}
# a key that sorts by name, then file, then line
function sc_key(name: pointer, file: pointer, line: int) -> pointer {
var f = file
if f == null { f = "" }
let ln = itoa(1000000 + line)
return name + "\t" + f + "\t" + ln
}
# a declaration the program wrote (not the runtime's, not one the compiler made)
function sc_written(d: Node) -> bool {
return d.file != null and d.pos >= 0 and not is_runtime_file(d.file)
}
# ---- records ----------------------------------------------------------------------------------
function sc_records(b: Buf) -> void {
let ds = new []Node
let xs = new []int
let keys = new []pointer
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_COMP and d.s != null and sc_written(d) and gen_find(d.s) < 0 {
push(xs, len(ds))
push(keys, sc_key(sc_name(d.s), d.file, d.line))
push(ds, d)
}
i += 1
}
i = 0
while i < len(g_events) {
let e = g_events[i]
if e.s != null and sc_written(e) {
push(xs, len(ds))
push(keys, sc_key(sc_name(e.s), e.file, e.line))
push(ds, e)
}
i += 1
}
sc_sort(xs, keys)
i = 0
while i < len(xs) {
sc_record(b, ds[xs[i]], i == 0)
i += 1
}
}
function sc_record(b: Buf, d: Node, first: bool) -> void {
sc_sep(b, first, " ")
var kind = "property"
if d.kind == N_EVENT { kind = "event" } else if d.uns == 2 { kind = "state" }
buf_puts(b, "{\"name\": ")
jq_put(b, sc_name(d.s))
buf_puts(b, ", \"kind\": ")
jq_put(b, kind)
buf_puts(b, ", \"module\": ")
sc_str(b, sc_mod(d.file))
buf_puts(b, ", \"exported\": ")
sc_bool(b, d.vis == 1)
buf_puts(b, ", ")
sc_at(b, d.file, d.line, d.col)
buf_puts(b, ", \"type_params\": ")
if d.tps == null { buf_puts(b, "null") } else { jq_put(b, d.tps) }
buf_puts(b, ",\n \"doc\": ")
sc_str(b, sc_doc(d.file, d.pos))
buf_puts(b, ",\n \"fields\": [")
var n = 0
var k = 0
while k < len(d.kids) {
let f = d.kids[k]
if f.kind == N_FIELD and f.pos >= 0 {
sc_field(b, f, n == 0)
n += 1
}
k += 1
}
if n > 0 { buf_puts(b, "\n ") }
buf_puts(b, "]}")
}
function sc_field(b: Buf, f: Node, first: bool) -> void {
sc_sep(b, first, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, f.s)
buf_puts(b, ", \"type\": ")
jq_put(b, sc_ty(f.ty))
buf_puts(b, ", \"default\": ")
sc_str(b, sc_text(f.file, f.sp0, f.sp1))
buf_puts(b, ", ")
sc_at(b, f.file, f.line, f.col)
buf_puts(b, ", \"attributes\": ")
sc_attrs(b, f.at)
buf_puts(b, ", \"doc\": ")
sc_str(b, sc_doc(f.file, f.pos))
buf_puts(b, "}")
}
# ---- registries -------------------------------------------------------------------------------
function sc_registries(b: Buf) -> void {
let xs = new []int
let keys = new []pointer
var r = 0
while r < len(g_rg_name) {
let v = g_rg_var[r]
if v.file != null and not is_runtime_file(v.file) {
push(xs, r)
push(keys, sc_key(g_rg_name[r], v.file, v.line))
} else {
push(keys, "")
}
r += 1
}
sc_sort(xs, keys)
var i = 0
while i < len(xs) {
sc_registry(b, xs[i], i == 0)
i += 1
}
}
function sc_registry(b: Buf, r: int, first: bool) -> void {
let v = g_rg_var[r]
sc_sep(b, first, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, g_rg_name[r])
buf_puts(b, ", \"record\": ")
jq_put(b, sc_ty(g_rg_type[r]))
buf_puts(b, ", \"prefix\": ")
jq_put(b, g_rg_prefix[r])
buf_puts(b, ", \"source\": ")
sc_str(b, g_rg_from[r])
buf_puts(b, ", \"open\": ")
sc_bool(b, g_rg_open[r] == 1)
buf_puts(b, ", \"exported\": ")
sc_bool(b, v.vis == 1)
buf_puts(b, ", \"module\": ")
sc_str(b, sc_mod(v.file))
buf_puts(b, ",\n ")
sc_at(b, v.file, v.line, v.col)
buf_puts(b, ", \"attributes\": ")
sc_attrs(b, g_rg_at[r])
buf_puts(b, ", \"doc\": ")
sc_str(b, sc_doc(v.file, v.pos))
let order = reg_order(r)
buf_puts(b, ",\n \"count\": ")
buf_puti(b, len(order))
buf_puts(b, ",\n \"contributors\": [")
sc_contributors(b, order)
buf_puts(b, "],\n \"entries\": [")
var o = 0
while o < len(order) {
sc_entry(b, r, order[o], o)
o += 1
}
if len(order) > 0 { buf_puts(b, "\n ") }
buf_puts(b, "]}")
}
# who brought an entry in: a resource file by the declaration that read it, `def`s by their file
function sc_contrib_id(d: int) -> pointer {
if len(g_df_src[d]) == 0 { return "def\n" + g_df_rec[d].file }
return g_df_src[d] + "\n" + g_df_via[d]
}
# which file brought which entries in, in the order they first appear in the registry
function sc_contributors(b: Buf, order: []int) -> void {
let seen = new []pointer
var o = 0
while o < len(order) {
let d = order[o]
let id = sc_contrib_id(d)
var known = false
var j = 0
while j < len(seen) {
if (seen[j] == id) { known = true }
j += 1
}
if not known {
push(seen, id)
if len(seen) > 1 { buf_puts(b, ",") }
buf_puts(b, "\n {\"kind\": ")
var file = g_df_src[d]
if len(file) == 0 {
jq_put(b, "def")
file = g_df_rec[d].file
} else {
jq_put(b, "resource")
}
buf_puts(b, ", \"file\": ")
sc_str(b, file)
buf_puts(b, ", \"via\": ")
jq_put(b, g_df_via[d])
buf_puts(b, ", \"module\": ")
sc_str(b, sc_mod(g_df_rec[d].file))
buf_puts(b, ", \"keys\": [")
var n = 0
var p = o
while p < len(order) {
let e = order[p]
if (sc_contrib_id(e) == id) {
if n > 0 { buf_puts(b, ", ") }
jq_put(b, g_df_key[e])
n += 1
}
p += 1
}
buf_puts(b, "]}")
}
o += 1
}
if len(seen) > 0 { buf_puts(b, "\n ") }
}
function sc_entry(b: Buf, r: int, d: int, index: int) -> void {
let rec = g_df_rec[d]
sc_sep(b, index == 0, " ")
buf_puts(b, "{\"key\": ")
jq_put(b, g_df_key[d])
buf_puts(b, ", \"constant\": ")
jq_put(b, `{g_rg_prefix[r]}_{reg_upper(g_df_key[d])}`)
buf_puts(b, ", \"index\": ")
buf_puti(b, index)
buf_puts(b, ", ")
sc_at(b, rec.file, g_df_kline[d], g_df_kcol[d])
buf_puts(b, ", \"fields\": [")
var n = 0
var k = 0
while k < len(rec.kids) {
let fi = rec.kids[k]
if fi.kind == E_FINIT and fi.sp0 >= 0 {
if n > 0 { buf_puts(b, ",") }
buf_puts(b, "\n {\"name\": ")
jq_put(b, fi.s)
buf_puts(b, ", \"value\": ")
sc_str(b, sc_text(fi.file, fi.sp0, fi.sp1))
buf_puts(b, ", ")
sc_at(b, fi.file, fi.line, fi.col)
buf_puts(b, "}")
n += 1
}
k += 1
}
if n > 0 { buf_puts(b, "\n ") }
buf_puts(b, "]}")
}
# ---- consts -----------------------------------------------------------------------------------
function sc_consts(b: Buf) -> void {
let xs = new []int
let keys = new []pointer
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_CONST and d.s != null and sc_written(d) { push(xs, i); push(keys, sc_key(sc_name(d.s), d.file, d.line)) }
else { push(keys, "") }
i += 1
}
sc_sort(xs, keys)
i = 0
while i < len(xs) {
let d = prog[xs[i]]
sc_sep(b, i == 0, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, sc_name(d.s))
buf_puts(b, ", \"type\": ")
jq_put(b, sc_ty(d.ty))
buf_puts(b, ", \"value\": ")
sc_str(b, sc_text(d.file, d.sp0, d.sp1))
buf_puts(b, ", \"module\": ")
sc_str(b, sc_mod(d.file))
buf_puts(b, ", \"exported\": ")
sc_bool(b, d.vis == 1)
buf_puts(b, ", ")
sc_at(b, d.file, d.line, d.col)
buf_puts(b, ", \"doc\": ")
sc_str(b, sc_doc(d.file, d.pos))
buf_puts(b, "}")
i += 1
}
}
# ---- functions a fn value can name ------------------------------------------------------------
function sc_is_state_param(p: Node) -> bool {
return is_state_ty(p.ty) or is_state_ty(sc_name(p.ty))
}
function sc_generic_fn(names: []pointer, s: pointer) -> bool {
let base = vis_plain(s)
var i = 0
while i < len(names) {
if (names[i] == base) { return true }
i += 1
}
return false
}
function sc_functions(b: Buf) -> void {
let gens = new []pointer
var i = 0
while i < len(prog) {
if prog[i].kind == N_FN and prog[i].tps != null and prog[i].s != null { push(gens, vis_plain(prog[i].s)) }
i += 1
}
let xs = new []int
let keys = new []pointer
i = 0
while i < len(prog) {
let d = prog[i]
var ok = d.kind == N_FN and d.s != null and d.tps == null and sc_written(d)
if ok { ok = not sc_generic_fn(gens, d.s) }
if ok {
var k = 0
while k < len(d.kids) {
if d.kids[k].kind == N_PARAM and not sc_is_state_param(d.kids[k]) { ok = false }
k += 1
}
}
if ok { push(xs, i); push(keys, sc_key(sc_name(d.s), d.file, d.line)) }
else { push(keys, "") }
i += 1
}
sc_sort(xs, keys)
i = 0
while i < len(xs) {
sc_function(b, prog[xs[i]], i == 0)
i += 1
}
}
function sc_function(b: Buf, d: Node, first: bool) -> void {
let ret = sc_ty(d.ty)
var ps: pointer = ""
var states: pointer = ""
var k = 0
while k < len(d.kids) {
let p = d.kids[k]
if p.kind == N_PARAM {
var mt: pointer = ""
if p.uns == 1 { mt = "mut " }
if len(ps) > 0 { ps = ps + ", " }
ps = ps + p.s + ": " + mt + sc_ty(p.ty)
let bq = buf_new()
jq_put(bq, sc_ty(p.ty))
if len(states) > 0 { states = states + ", " }
states = states + buf_str(bq)
}
k += 1
}
sc_sep(b, first, " ")
buf_puts(b, "{\"name\": ")
jq_put(b, sc_name(d.s))
buf_puts(b, ", \"module\": ")
sc_str(b, sc_mod(d.file))
buf_puts(b, ", \"exported\": ")
sc_bool(b, d.vis == 1)
buf_puts(b, ", ")
sc_at(b, d.file, d.line, d.col)
buf_puts(b, ", \"returns\": ")
jq_put(b, ret)
buf_puts(b, ", \"fn_type\": ")
jq_put(b, `fn() -> {ret}`)
buf_puts(b, ", \"signature\": ")
jq_put(b, `function {sc_name(d.s)}({ps}) -> {ret}`)
buf_puts(b, ", \"states\": [")
buf_puts(b, states)
buf_puts(b, "], \"doc\": ")
sc_str(b, sc_doc(d.file, d.pos))
buf_puts(b, "}")
}

View file

@ -61,7 +61,8 @@ function vis_check(d: Node, what0: pointer) -> void {
if vis_reporting() {
vis_say(`uses: {d.file}:{itoa(d.line)}: {uto}.{what} used from {here} (module {from})\n`)
} else {
perr(`{from} uses {uto}.{what} ({d.file}:{itoa(d.line)}): add 'uses {uto}' to {from}'s module line, or take it through a port`)
let um = `{from} uses {uto}.{what} ({d.file}:{itoa(d.line)}): add 'uses {uto}' to {from}'s module line, or take it through a port`
if g_diag_json { diag_add(here, g_err_line, 0, "error", um) } else { perr(um) }
}
}
let to = module_of(d.file)
@ -72,5 +73,10 @@ function vis_check(d: Node, what0: pointer) -> void {
vis_say(`vis: {d.file}:{itoa(d.line)}: {to}.{what} used from {here}\n`)
return
}
perr(`{what} is private to module {to}; mark it 'export' where it is declared ({d.file})`)
let pm = `{what} is private to module {to}; mark it 'export' where it is declared ({d.file})`
if g_diag_json {
diag_add(here, g_err_line, 0, "error", pm)
return
}
perr(pm)
}

View file

@ -41,9 +41,16 @@ function ck_reporting() -> bool {
function ck_err(cat: pointer, n: Node, msg: pointer) -> void {
var file: pointer = ""
var line = 0
var col = 0
if n != null {
if n.file != null { file = n.file }
line = n.line
col = n.col
}
if g_diag_json and not ck_reporting() {
diag_add(file, line, col, "error", msg)
ck_errors += 1
return
}
var m = `{file}:{itoa(line)}: error: {msg}\n`
if ck_reporting() { m = `check[{cat}]: {file}:{itoa(line)}: {msg}\n` }

View file

@ -337,6 +337,7 @@ function check_program() -> void {
}
gen_finish()
if ck_errors > 0 and not ck_reporting() and not g_migrate {
if g_diag_json { diag_exit(1) }
let m = `{itoa(ck_errors)} type error(s)\n`
file_write(file_stderr(), m, len(m))
exit(1)

View file

@ -94,6 +94,10 @@ property Node {
mg: int = -1 # 0.S2: its index in the migration's tables (migrate.ludic)
vw: int = -1 # 0.S: the view a function was generated for or written in
cm: int = -1 # 0.S: the component a function was generated for or written in
col: int = 0 # the column of its first token (1-based; 0 when generated)
at: int = -1 # its attributes (@Ref(Items), @Range(0, 1), ...): a group in attrs.ludic
sp0: int = -1 # a field's default, a const's value, an entry field's value: where its
sp1: int = -1 # source text starts and ends in its file (the schema's text of it)
}
# every node remembers where it was parsed (file + the line of the token the
@ -105,6 +109,7 @@ function node(kind: int) -> Node {
n.file = g_parse_file
if g_parsing and pi < len(toks) {
n.line = toks[pi].line
n.col = toks[pi].col
n.pos = toks[pi].pos
if g_gen_nodes { n.pos = -1 } # made by the compiler, not read from a file
}

View file

@ -0,0 +1,164 @@
# attrs.ludic — what an editor needs to know about a field or a registry, said on the @-syntax the
# compiler already reads (@max(64), @owns(Kind)) and carried into `--emit-schema`:
#
# property Item {
# @Ref(Vendors) seller: int = 0 # an index into that registry (its entries are offered)
# @OneOf(GR_) grade: int = 0 # one of the constants that start GR_
# @Range(0, 1.5) weight: float = 0.4
# @Unit("m/s") speed: float = 0.0
# @Asset("gltf") model: string = "" # a file of that kind
# @Color tint: int = 0
# }
# @AppendOnly @ByKey registry Items of Item as IT from "items.lres"
#
# They change nothing the program does. One is checked: @Ref must name a registry that exists. A
# field's or a registry's attributes are a group (Node.at, g_rg_at): a run of names, each with its
# arguments as JSON values - an identifier or a string as a string, a number as written.
var g_at_start: []int = new []int # a group's first attribute ...
var g_at_count: []int = new []int # ... and how many it holds
var g_at_name: []pointer = new []pointer
var g_at_args: []pointer = new []pointer # the arguments, a JSON array: ["Items"], [0, 1.5]
var g_at_first: []pointer = new []pointer # the first argument as written (@Ref's registry), or null
var g_at_nargs: []int = new []int
var g_at_open: int = -1 # the group the attributes being read go into
var g_at_decl: []pointer = new []pointer # @AppendOnly / @ByKey read before a declaration
var g_at_keep: bool = false # `export` parses its declaration one call down
# a new group, empty; its index
function at_group() -> int {
push(g_at_start, len(g_at_name))
push(g_at_count, 0)
return len(g_at_start) - 1
}
function at_push(g: int, name: pointer, args: pointer, first: pointer, nargs: int) -> void {
push(g_at_name, name)
push(g_at_args, args)
push(g_at_first, first)
push(g_at_nargs, nargs)
g_at_count[g] = g_at_count[g] + 1
}
# `(a, "b", -1.5)` after an attribute's name, if it is there, as a JSON array; the first argument
# as written goes to g_at_arg0, the count to g_at_argn
var g_at_arg0: pointer = null
var g_at_argn: int = 0
var g_at_allnum: bool = true # every argument a number
function at_args() -> pointer {
g_at_arg0 = null
g_at_argn = 0
g_at_allnum = true
if not is_op("(") { return "[]" }
pi += 1
var out: pointer = "["
while not is_op(")") {
skipnl()
var neg = false
if is_op("-") { neg = true; pi += 1 }
let t = toks[pi]
var v: pointer = null
var raw: pointer = null
if t.kind == TK_INT {
raw = itoa(t.ival)
if t.text != null { raw = t.text }
if neg { raw = "-" + raw }
v = raw
} else if t.kind == TK_FLOAT {
raw = t.text
if neg { raw = "-" + raw }
v = raw
if raw[0] == '.' or (neg and raw[1] == '.') { v = at_lead0(raw) }
} else if (t.kind == TK_STR or t.kind == TK_ID) and not neg {
g_at_allnum = false
raw = t.text
let b = buf_new()
jq_put(b, raw)
v = buf_str(b)
} else {
perr(`an attribute's argument is a name, a string or a number, not {tok_desc()}`)
}
pi += 1
if g_at_argn > 0 { out = out + ", " }
out = out + v
if g_at_argn == 0 { g_at_arg0 = raw }
g_at_argn += 1
skipnl()
if is_op(",") { pi += 1 }
}
eat_op(")")
return out + "]"
}
function at_lead0(raw: pointer) -> pointer {
if raw[0] == '-' { return "-0" + raw[1 .. len(raw)] }
return "0" + raw
}
# the editor attributes a field may carry, and what each takes
function at_field_known(a: pointer) -> bool {
return (a == "Ref") or (a == "OneOf") or (a == "Range") or (a == "Unit") or (a == "Asset") or (a == "Color")
}
# one field attribute, its '@' and name already read: its arguments, checked for shape, into group g
function at_field(g: int, a: pointer) -> void {
let args = at_args()
let n = g_at_argn
if (a == "Ref") or (a == "OneOf") {
if n != 1 or toks[pi - 2].kind != TK_ID { perr(`@{a} takes one name: @{a}(Items)`) }
}
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 == "Color") and n != 0 { perr("@Color takes nothing") }
at_push(g, a, args, g_at_arg0, n)
}
# a field's group, made the first time it needs one
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
function at_decl(a: pointer) -> void {
push(g_at_decl, a)
}
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 }
}
# the registry just declared takes the attributes read before it
function at_registry_take() -> int {
if len(g_at_decl) == 0 { return -1 }
let g = at_group()
var i = 0
while i < len(g_at_decl) {
at_push(g, g_at_decl[i], "[]", null, 0)
i += 1
}
g_at_decl = new []pointer
return g
}
# once every registry is declared: each @Ref names one, or the build stops with every one that does not
function at_check_refs() -> void {
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_COMP {
var k = 0
while k < len(d.kids) {
let f = d.kids[k]
if f.at >= 0 { at_check_field(d, f) }
k += 1
}
}
i += 1
}
diag_stop_if_errors()
}
function at_check_field(d: Node, f: Node) -> void {
let g = f.at
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 {
perr_at(f.file, f.line, f.col, `field {d.s}.{f.s}: @Ref({g_at_first[j]}) names no registry - there is no registry {g_at_first[j]}`)
}
j += 1
}
}

View file

@ -0,0 +1,95 @@
# diag.ludic — `--diagnostics=json`: every error and warning the compiler reports, collected and
# written to stdout as one JSON array of {file, line, col, severity, message} when it exits, instead
# of the `file:line: error: message` lines on stderr. The type checker and the module rules (vis,
# uses) report everything they find; an error the parser or the emitter cannot go on from (a token
# it did not expect, an unknown name while lowering) is the last one in the array. A col of 0 is a
# place known only to its line.
var g_diag_json: bool = false
var g_dg_file: []pointer = new []pointer
var g_dg_line: []int = new []int
var g_dg_col: []int = new []int
var g_dg_sev: []pointer = new []pointer
var g_dg_msg: []pointer = new []pointer
var g_dg_errors: int = 0
# one diagnostic, kept for the array - once, though a pass may find it twice (a reference the
# checker and the emitter both resolve)
function diag_add(file: pointer, line: int, col: int, sev: pointer, msg: pointer) -> void {
var f = file
if f == null { f = "" }
let m = diag_trim(msg)
var i = 0
while i < len(g_dg_file) {
if g_dg_line[i] == line and g_dg_col[i] == col and (g_dg_msg[i] == m) and (g_dg_file[i] == f) and (g_dg_sev[i] == sev) { return }
i += 1
}
push(g_dg_file, f)
push(g_dg_line, line)
push(g_dg_col, col)
push(g_dg_sev, sev)
push(g_dg_msg, m)
if (sev == "error") { g_dg_errors += 1 }
}
# a message without its closing newline
function diag_trim(m: pointer) -> pointer {
var n = len(m)
while n > 0 and (m[n - 1] == '\n' or m[n - 1] == '\r') { n -= 1 }
return m[0 .. n]
}
# `s` as a JSON string, quotes included, into b
function jq_put(b: Buf, s: pointer) -> void {
buf_putc(b, '"')
if s != null {
var i = 0
while s[i] != 0 {
let c = s[i]
if c == '"' { buf_puts(b, "\\\"") }
else if c == CH_BACKSLASH { buf_puts(b, "\\\\") }
else if c == '\n' { buf_puts(b, "\\n") }
else if c == '\t' { buf_puts(b, "\\t") }
else if c == '\r' { buf_puts(b, "\\r") }
else if c >= 0 and c < 32 {
buf_puts(b, "\\u00")
buf_putc(b, jq_hex(c >> 4))
buf_putc(b, jq_hex(c & 15))
}
else { buf_putc(b, c) }
i += 1
}
}
buf_putc(b, '"')
}
function jq_hex(d: int) -> int {
if d < 10 { return 48 + d }
return 87 + d
}
# the array, on stdout
function diag_flush() -> void {
let b = buf_new()
buf_puts(b, "[")
var i = 0
while i < len(g_dg_file) {
if i > 0 { buf_puts(b, ",") }
buf_puts(b, "\n {\"file\": ")
jq_put(b, g_dg_file[i])
buf_puts(b, ", \"line\": ")
buf_puti(b, g_dg_line[i])
buf_puts(b, ", \"col\": ")
buf_puti(b, g_dg_col[i])
buf_puts(b, ", \"severity\": ")
jq_put(b, g_dg_sev[i])
buf_puts(b, ", \"message\": ")
jq_put(b, g_dg_msg[i])
buf_puts(b, "}")
i += 1
}
if len(g_dg_file) > 0 { buf_puts(b, "\n") }
buf_puts(b, "]\n")
let out = file_stdout()
file_write(out, buf_str(b), b.len)
}
# stop: with --diagnostics=json the array is written first
function diag_exit(code: int) -> void {
if g_diag_json { diag_flush() }
exit(code)
}

View file

@ -11,12 +11,15 @@ const TK_EOF: int = 5
const TK_FLOAT: int = 6
const TK_INTERP: int = 7 # `text {expr} text` — raw content, split by the parser
property Tok { kind: int = 0, text: pointer = null, ival: int = 0, line: int = 0, pos: int = -1, end: int = -1 }
property Tok { kind: int = 0, text: pointer = null, ival: int = 0, line: int = 0, pos: int = -1, end: int = -1, col: int = 0, off: int = -1, oend: int = -1 }
# col: the token's column (1-based) on its line; off / oend: where it starts and ends in the text
# lexed, whatever the base (a resource file's own offsets, for the schema's spans - never a rewrite)
# where the tokens are in their file (0.S's migration rewrites source by position): a file's own
# text starts at 0, an interpolation hole at its place in the file, and generated code has none (-1)
var g_lex_base: int = -1
var lx_start: int = 0
var lx_line0: int = 0 # where the line being lexed starts in the text
var toks: []Tok
@ -24,6 +27,8 @@ function tok_push(kind: int, text: pointer, ival: int, line: int) -> void {
let t = new Tok
t.kind = kind; t.text = text; t.ival = ival; t.line = line
if g_lex_base >= 0 { t.pos = g_lex_base + lx_start }
t.off = lx_start
t.col = lx_start - lx_line0 + 1
push(toks, t)
}
@ -103,6 +108,10 @@ function hole_end(s: pointer, i0: int, n: int) -> int {
# report a lexical error in the standard `file:line: error: msg` shape and stop
function lex_fail(line: int, msg: pointer) -> void {
if g_diag_json {
diag_add(g_parse_file, line, lx_start - lx_line0 + 1, "error", msg)
diag_exit(1)
}
let m = `{g_parse_file}:{line}: error: {msg}\n`
file_write(file_stderr(), m, len(m))
exit(1)
@ -119,6 +128,7 @@ function lex(src: pointer) -> void {
# re-lexed on its own, and keeps the line of the string it sits in)
function lex_at(src: pointer, first_line: int) -> void {
toks = new []Tok
lx_line0 = 0
var i = 0
var line = first_line
let n = len(src)
@ -127,11 +137,12 @@ function lex_at(src: pointer, first_line: int) -> void {
# the token the last turn pushed ends where this one starts looking
if len(toks) > lx_n {
if g_lex_base >= 0 { toks[len(toks) - 1].end = g_lex_base + i }
toks[len(toks) - 1].oend = i
lx_n = len(toks)
}
lx_start = i
let c = src[i]
if c == '\n' { tok_push(TK_NL, null, 0, line); line += 1; i += 1; continue }
if c == '\n' { tok_push(TK_NL, null, 0, line); line += 1; i += 1; lx_line0 = i; continue }
if c == ' ' or c == '\t' or c == '\r' { i += 1; continue }
if c == '#' { # '#' comment to end of line
while i < n and src[i] != '\n' { i += 1 }
@ -255,6 +266,7 @@ function lex_at(src: pointer, first_line: int) -> void {
lex_error(line, c)
}
if len(toks) > lx_n and g_lex_base >= 0 { toks[len(toks) - 1].end = g_lex_base + i }
if len(toks) > lx_n { toks[len(toks) - 1].oend = i }
lx_start = i
tok_push(TK_EOF, null, 0, line)
}

View file

@ -75,6 +75,7 @@ function fm_get(m: FileMap, f: pointer) -> pointer {
var g_fm_mod: FileMap = fm_new()
var g_fm_pkg: FileMap = fm_new()
var g_fm_trusted: FileMap = fm_new()
var g_fm_src: FileMap = fm_new() # every file read, by its path: the schema's docs and spans
function trust_file(f: pointer) -> void {
push(g_trusted_files, f)
fm_put(g_fm_trusted, f, "1")
@ -110,16 +111,36 @@ function is_kw(v: pointer) -> bool { return is_id(v) }
function perr(msg: pointer) -> void {
var file = g_err_file
var line = g_err_line
var col = 0
if g_parsing {
file = g_parse_file
line = 0
if pi < len(toks) { line = toks[pi].line }
if pi < len(toks) { line = toks[pi].line; col = toks[pi].col }
}
if file == null { file = "" }
if g_diag_json {
diag_add(file, line, col, "error", msg)
diag_exit(1)
}
let m = `{file}:{line}: error: {msg}\n`
file_write(file_stderr(), m, len(m))
exit(1)
}
# an error the pass reporting it can go on from (every @Ref, every module rule broken): reported,
# counted, and the build stops once the pass is done (diag_stop_if_errors)
function perr_at(file: pointer, line: int, col: int, msg: pointer) -> void {
var f = file
if f == null { f = "" }
if g_diag_json { diag_add(f, line, col, "error", msg) }
else {
let m = `{f}:{itoa(line)}: error: {msg}\n`
file_write(file_stderr(), m, len(m))
g_dg_errors += 1
}
}
function diag_stop_if_errors() -> void {
if g_dg_errors > 0 { diag_exit(1) }
}
# the current token, described for a diagnostic
function tok_desc() -> pointer {
@ -444,7 +465,11 @@ function record() -> Node {
eat_op("{")
let r = node(E_REC)
while true { skipnl(); if is_op("}") { break }
let fi = node(E_FINIT); fi.s = eat_id(); eat_op(":"); fi.a = expr(); push(r.kids, fi)
let fi = node(E_FINIT); fi.s = eat_id(); eat_op(":")
let v0 = pi
fi.a = expr()
fi.sp0 = toks[v0].off; fi.sp1 = toks[pi - 1].oend # the schema's text of the value
push(r.kids, fi)
if is_op(",") { pi += 1 } }
eat_op("}"); return r
}
@ -598,7 +623,10 @@ function parse_var() -> Node {
return n
}
function parse_const() -> Node {
pi += 1; let n = node(N_CONST); n.s = eat_id(); eat_op(":"); n.ty = ptype(); eat_op("="); n.a = expr()
pi += 1; let n = node(N_CONST); n.s = eat_id(); eat_op(":"); n.ty = ptype(); eat_op("=")
let v0 = pi
n.a = expr()
n.sp0 = toks[v0].off; n.sp1 = toks[pi - 1].oend # the schema's text of the value
return n
}
# the language's statement / declaration words cannot name a function (`new`,
@ -915,6 +943,7 @@ function already_loaded(full: pointer) -> bool {
# Modifiers are `@annotations` in front of the declaration: `@export`, `@edge`,
# `@pure`, `@deterministic`, … — one channel, not a zoo of prefix keywords.
function parse_one_decl() -> void {
at_decl_reset() # attrs.ludic: @AppendOnly / @ByKey belong to one declaration
var is_export = false
var qspec: Node = null
var onspawn_model: pointer = null
@ -964,6 +993,7 @@ function parse_one_decl() -> void {
else if a == "EngineSystem" { eat_op("("); esys_comp = eat_id(); eat_op(","); esys_phase = eat_id(); eat_op(")") } # @EngineSystem(Comp, Phase) package engine system (#62)
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 == "alloc_ok" { # 25.2: @alloc_ok("why") - this allocates in a frame on purpose
eat_op("(")
let why = expr()
@ -1000,6 +1030,7 @@ function parse_one_decl() -> void {
# `export function f`, `export var v`, ...: visible from other modules (L3)
if is_id("export") and (toks[pi + 1].kind == TK_ID) {
pi += 1
g_at_keep = true
let p0 = len(prog)
let e0 = len(g_events)
parse_one_decl()
@ -1249,6 +1280,7 @@ function do_import(rel: pointer) -> void {
}
if already_loaded(full) { return }
push(loaded_paths, full)
fm_put(g_fm_src, full, src)
if is_float_file(g_parse_file) and not is_runtime_path(rel) and not is_float_file(full) { float_file_add(full) }
# a module reaches as far as its own files: a package found through $LUDIC_HOME or
# ludic_modules is not beside its importer and keeps its own module (or none)
@ -1645,6 +1677,7 @@ function parse_program() -> void {
g_gen_nodes = true # 0.S: what the finishing passes make is generated
modules_finish() # L3: the declared uses have no cycle
registries_finish() # L8: each registry gets its defs
at_check_refs() # every @Ref names a registry (attrs.ludic)
ports_finish() # L3: each port gets its one bind
views_finish() # L11: each view gets its model and call
components_finish() # L11: each component gets its class

View file

@ -11,14 +11,21 @@ function parse_component() -> Node {
var is_sync = false # @Sync — this field replicates (NETWORKING N2)
var is_frame = false # @frame — a fn stored here runs every frame (25.2)
var cap = 0 # @max(n) — this list never holds more than n (25.5a)
if is_op("@") {
var fat = -1 # its attributes, for the schema (attrs.ludic)
while is_op("@") { # one or more, on its line or the lines above
pi += 1
let ann = eat_id()
if ann == "Computed" { is_computed = true } else if ann == "Sync" { is_sync = true } else if ann == "frame" { is_frame = true }
if (ann == "Computed") or (ann == "Sync") or (ann == "frame") {
if ann == "Computed" { is_computed = true } else if ann == "Sync" { is_sync = true } else { is_frame = true }
if fat < 0 { fat = at_group() }
at_push(fat, ann, "[]", null, 0)
}
else if ann == "owns" { # @owns(PhysShape) — this field holds a handle its record owns (25.5e)
eat_op("(")
g_own_pending = eat_id()
eat_op(")")
if fat < 0 { fat = at_group() }
at_push(fat, ann, `["{g_own_pending}"]`, g_own_pending, 1)
}
else if ann == "max" {
eat_op("(")
@ -26,14 +33,30 @@ function parse_component() -> Node {
if ce.kind != E_INT or ce.ival <= 0 { perr("@max takes the most the list may hold: @max(64)") }
cap = ce.ival
eat_op(")")
if fat < 0 { fat = at_group() }
at_push(fat, ann, `[{itoa(cap)}]`, itoa(cap), 1)
}
else if at_field_known(ann) { # @Ref(Items), @Range(0, 1), @Unit("m/s"), ...
if fat < 0 { fat = at_group() }
at_field(fat, ann)
}
else if is_op("(") { # anything else: read past, as a declaration's is
var d = 0
while true { if is_op("(") { d += 1 }; if is_op(")") { d -= 1 }; pi += 1; if d == 0 { break } }
}
skipnl()
}
let f = node(N_FIELD); f.s = eat_id(); eat_op(":"); f.ty = ptype()
f.at = fat
if is_frame { push(g_frame_fields, `{n.s}.{f.s}`) }
if cap > 0 { push(g_cap_keys, `{n.s}.{f.s}`); push(g_cap_vals, cap) }
if g_own_pending != null { push(g_own_type, n.s); push(g_own_field, f.s); push(g_own_kind, g_own_pending); g_own_pending = null }
if is_op("=") { pi += 1; f.a = expr() }
if is_op("=") {
pi += 1
let v0 = pi
f.a = expr()
f.sp0 = toks[v0].off; f.sp1 = toks[pi - 1].oend # the schema's text of the default
}
if is_sync { f.ival = 1 } # mark the field replicable (read by emit_net)
if is_computed { register_computed(n.s, f.s, f.ty, f.a) } # derived: no storage
else { push(n.kids, f) }
@ -52,7 +75,12 @@ function parse_event() -> Node {
n.s = eat_id(); skipnl(); eat_op("{")
while true { skipnl(); if is_op("}") { break }
let f = node(N_FIELD); f.s = eat_id(); eat_op(":"); f.ty = ptype()
if is_op("=") { pi += 1; f.a = expr() }
if is_op("=") {
pi += 1
let v0 = pi
f.a = expr()
f.sp0 = toks[v0].off; f.sp1 = toks[pi - 1].oend
}
push(n.kids, f)
if is_op(",") { pi += 1 } }
eat_op("}"); return n

View file

@ -16,9 +16,25 @@ var g_rg_type: []pointer = new []pointer
var g_rg_prefix: []pointer = new []pointer
var g_rg_var: []Node = new []Node
var g_rg_open: []int = new []int # 1: `open registry` - other modules may def into it
var g_rg_from: []pointer = new []pointer # its resource file as written, or null
var g_rg_at: []int = new []int # its attributes (@AppendOnly, @ByKey), a group or -1
var g_df_reg: []pointer = new []pointer
var g_df_key: []pointer = new []pointer
var g_df_rec: []Node = new []Node
var g_df_kline: []int = new []int # where its key is written (the schema's)
var g_df_kcol: []int = new []int
var g_df_src: []pointer = new []pointer # the resource file it came from, or "" for a `def`
var g_df_via: []pointer = new []pointer # the declaration that brought it in: file:line
# one def, and where it came from
function df_push(reg: pointer, key: pointer, rec: Node, kline: int, kcol: int, src: pointer, via: pointer) -> void {
push(g_df_reg, reg)
push(g_df_key, key)
push(g_df_rec, rec)
push(g_df_kline, kline)
push(g_df_kcol, kcol)
push(g_df_src, src)
push(g_df_via, via)
}
function reg_upper(s: pointer) -> pointer {
let n = len(s)
@ -72,6 +88,8 @@ function parse_registry() -> void {
push(g_rg_prefix, prefix)
push(g_rg_var, v)
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) }
}
# def REGISTRY key { field: value, ... }
@ -85,12 +103,11 @@ function parse_def() -> void {
res_read_into(reg, from, `def {reg} from`)
return
}
let kt = toks[pi]
let key = eat_id()
skipnl()
let rec = record()
push(g_df_reg, reg)
push(g_df_key, key)
push(g_df_rec, rec)
df_push(reg, key, rec, kt.line, kt.col, "", `{g_parse_file}:{itoa(kt.line)}`)
}
function reg_find(name: pointer) -> int {
var i = 0

View file

@ -28,6 +28,10 @@ function res_read_into(reg: pointer, from: pointer, who: pointer) -> void {
let saved_pi = pi
let saved_file = g_parse_file
let saved_parsing = g_parsing
var via_line = 0
if saved_pi > 0 { via_line = saved_toks[saved_pi - 1].line }
let via = `{saved_file}:{itoa(via_line)}`
fm_put(g_fm_src, path, src) # the schema's spans and docs read it again
if not (module_of(saved_file) == "") { module_set(path, module_of(saved_file)) }
if is_float_file(saved_file) and not is_float_file(path) { float_file_add(path) }
g_parse_file = path
@ -38,12 +42,11 @@ function res_read_into(reg: pointer, from: pointer, who: pointer) -> void {
while true {
skipnl()
if toks[pi].kind == TK_EOF { break }
let kt = toks[pi]
let key = eat_id()
skipnl()
let rec = record()
push(g_df_reg, reg)
push(g_df_key, key)
push(g_df_rec, rec)
df_push(reg, key, rec, kt.line, kt.col, path, via)
}
g_res_mode = false
toks = saved_toks

View file

@ -55,6 +55,10 @@ function ensure_slash(d: pointer) -> pointer {
}
function die(msg: pointer) -> void {
if g_diag_json {
diag_add("", 0, 0, "error", msg)
diag_exit(1)
}
file_write(file_stderr(), msg, len(msg))
exit(1)
}
@ -154,6 +158,9 @@ 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 == "--diagnostics=json" { g_diag_json = true } # every error, as JSON on stdout (diag.ludic)
else if a == "--diagnostics=text" { g_diag_json = false }
else if a == "--emit-schema" { ai += 1; if ai < arg_count() { g_schema_out = arg(ai) } } # emit_schema.ludic
else if a == "--globals" { g_allow_globals = true }
else if a == "--migrate-state" { # 0.S2: write the plan ludic migrate state applies
ai += 1
@ -194,6 +201,7 @@ entry {
let src = read_file(path)
if (src == null) { die("ludicc: cannot open input\n") }
fm_put(g_fm_src, path, src) # the schema reads docs and spans from it
cur_dir = dir_of(path)
g_src_name = base_name(path) # for panic/expect file:line messages
g_parse_file = path # for the compiler's own file:line diagnostics
@ -238,13 +246,21 @@ entry {
file_close(sink)
}
if g_migrate { mg_finish() }
# --emit-schema FILE: the declarations, the registries and their entries as the checker resolved
# them (emit_schema.ludic); on its own it stops there, with -o or --check the build goes on
if g_schema_out != null {
if not schema_write(g_schema_out) { die(`ludicc: cannot write {g_schema_out}\n`) }
if not g_check_only and not has_target and not emit_ir { diag_exit(0) }
}
# --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 {
emit_program()
exit(0)
if g_dg_errors > 0 { diag_exit(1) }
diag_exit(0)
}
emit_program()
if g_dg_errors > 0 { diag_exit(1) } # errors a pass went on from (diag.ludic)
deps_flush() # LUDIC_DEPS=<file>: the module graph (ludic deps)
# --emit-llvm, or no binary target: emit IR and stop (stdout when out is null).