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:
parent
f8d811aa97
commit
42deb76c28
27 changed files with 1220 additions and 23 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
164
selfhost/frontend/attrs.ludic
Normal file
164
selfhost/frontend/attrs.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
95
selfhost/frontend/diag.ludic
Normal file
95
selfhost/frontend/diag.ludic
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue