--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>
145 lines
4.4 KiB
Text
145 lines
4.4 KiB
Text
# registry.ludic — L8: registration by declaration. A registry is a global table of records,
|
|
# and each `def` is one entry, written where it belongs rather than as a call in an init
|
|
# function somebody has to remember:
|
|
#
|
|
# registry Furnishings of Furnishing as HF
|
|
# def Furnishings chair { name: "Camp chair", cost: 60, seat: true }
|
|
#
|
|
# The parser collects every def (in source order, across files) and, when the program is read,
|
|
# gives the registry its entries as its initializer, a constant per entry holding its index
|
|
# (HF_CHAIR - declaration order, so a table saved by position keeps its rule: append, never
|
|
# insert), HF_COUNT, and, when the record has a `key: string` field, fills it in and adds
|
|
# furnishings_find(key) -> int. Everything generated belongs to the registry's own file, so it
|
|
# is in the registry's module and exported only when the registry is.
|
|
var g_rg_name: []pointer = new []pointer
|
|
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)
|
|
let out = bytes(n + 1)
|
|
var i = 0
|
|
while i < n {
|
|
var c = s[i]
|
|
if c >= 'a' and c <= 'z' { c = c - 32 }
|
|
out[i] = c
|
|
i += 1
|
|
}
|
|
out[n] = 0
|
|
return out
|
|
}
|
|
function reg_lower(s: pointer) -> pointer {
|
|
let n = len(s)
|
|
let out = bytes(n + 1)
|
|
var i = 0
|
|
while i < n {
|
|
var c = s[i]
|
|
if c >= 'A' and c <= 'Z' { c = c + 32 }
|
|
out[i] = c
|
|
i += 1
|
|
}
|
|
out[n] = 0
|
|
return out
|
|
}
|
|
# registry NAME of TYPE [as PREFIX]
|
|
function parse_registry() -> void {
|
|
pi += 1
|
|
let v = node(N_VAR)
|
|
v.s = eat_id()
|
|
if not is_id("of") { perr(`registry {v.s}: say what it holds - registry {v.s} of SomeRecord`) }
|
|
pi += 1
|
|
let t = ptype()
|
|
var prefix = reg_upper(v.s)
|
|
if is_id("as") {
|
|
pi += 1
|
|
prefix = eat_id()
|
|
}
|
|
var from: pointer = null
|
|
if is_id("from") {
|
|
pi += 1
|
|
from = toks[pi].text
|
|
pi += 1
|
|
}
|
|
v.ty = "[]" + t
|
|
push(prog, v)
|
|
push(g_rg_name, v.s)
|
|
push(g_rg_type, t)
|
|
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, ... }
|
|
# def REGISTRY from "file.lres" - every entry of a resource file, as defs of this file's module
|
|
function parse_def() -> void {
|
|
pi += 1
|
|
let reg = eat_id()
|
|
if is_id("from") and toks[pi + 1].kind == TK_STR {
|
|
let from = toks[pi + 1].text
|
|
pi += 2
|
|
res_read_into(reg, from, `def {reg} from`)
|
|
return
|
|
}
|
|
let kt = toks[pi]
|
|
let key = eat_id()
|
|
skipnl()
|
|
let rec = record()
|
|
df_push(reg, key, rec, kt.line, kt.col, "", `{g_parse_file}:{itoa(kt.line)}`)
|
|
}
|
|
function reg_find(name: pointer) -> int {
|
|
var i = 0
|
|
while i < len(g_rg_name) {
|
|
if (g_rg_name[i] == name) { return i }
|
|
i += 1
|
|
}
|
|
return -1
|
|
}
|
|
function reg_rec_has(rec: Node, field: pointer) -> bool {
|
|
var i = 0
|
|
while i < len(rec.kids) {
|
|
if (rec.kids[i].s == field) { return true }
|
|
i += 1
|
|
}
|
|
return false
|
|
}
|
|
# a generated declaration: the registry's file, line and export
|
|
function reg_decl(kind: int, v: Node) -> Node {
|
|
let d = node(kind)
|
|
d.file = v.file
|
|
d.line = v.line
|
|
d.vis = v.vis
|
|
return d
|
|
}
|
|
function reg_const(v: Node, name: pointer, value: int) -> void {
|
|
let c = reg_decl(N_CONST, v)
|
|
c.s = name
|
|
c.ty = "int"
|
|
let e = node(E_INT)
|
|
e.ival = value
|
|
e.file = v.file
|
|
c.a = e
|
|
push(prog, c)
|
|
}
|