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

@ -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