Merge commit '863b971' into lang/foundations
This commit is contained in:
commit
e83802f15e
29 changed files with 126663 additions and 106591 deletions
|
|
@ -73,6 +73,8 @@ point at a different LLVM toolchain if you have one.
|
|||
| a windowed native executable | `ludicc game.ludic -o build/game` |
|
||||
| a headless executable | `ludicc game.ludic --headless -o build/game` |
|
||||
| the IR, to read | `ludicc src.ludic --emit-llvm -o src.ll` |
|
||||
| the schema an editor reads (records, registries and their entries, consts) | `ludicc src.ludic --emit-schema schema.json` |
|
||||
| every error, as a JSON array on stdout | `ludicc src.ludic --check --diagnostics=json` |
|
||||
| a shared library † | `ludicc lib.ludic --shared -o build/liblib.dylib` |
|
||||
| a game that runs in a browser † | `ludicc game.ludic --target wasm32-unknown-unknown -o build/web/game.wasm` |
|
||||
| an object file † | `ludicc src.ludic -c -o src.o` |
|
||||
|
|
|
|||
53
LANGUAGE.md
53
LANGUAGE.md
|
|
@ -625,6 +625,49 @@ int and this is a string`). Its entries are defs of the module that wrote the li
|
|||
must be open (and exported) to it, and they take that module's place in the order: the declaring
|
||||
module's entries, then each other module's by module name, and within a file, file order.
|
||||
|
||||
### Editor attributes and the schema (`@Ref`, `@Range`, ..., `ludic schema`)
|
||||
|
||||
A field can say what an editor of the data should offer for it, and a registry how its entries may
|
||||
change, on the same `@` a field's `@max(64)` is written with - one or several, on the field's line or
|
||||
the lines above it:
|
||||
|
||||
```ludic
|
||||
# doc-check: skip — the registries it names are declared elsewhere
|
||||
property Tool {
|
||||
@Ref(Vendors) seller: int = 0 # an index into that registry: its entries are offered
|
||||
@OneOf(GR_) grade: int = 0 # one of the constants whose names start GR_
|
||||
@Range(0, 20.5) @Unit("kg") weight: float = 1.0
|
||||
@Asset("gltf") model: string = "" # a file of that kind (any string)
|
||||
@Color tint: int = 0
|
||||
}
|
||||
@AppendOnly @ByKey
|
||||
registry Tools of Tool as TL from "data/tools.lres"
|
||||
```
|
||||
|
||||
They change nothing the program does. One is checked: `@Ref` must name a registry the program
|
||||
declares, and a field whose `@Ref` names none is an error naming the field and the registry (every
|
||||
such field is reported, not only the first).
|
||||
|
||||
`ludicc app.ludic --emit-schema out.json` (or `ludic schema [file] [-o out.json]`) writes what the
|
||||
compiler resolved, once the types are checked and every open registry has its entries, as one JSON
|
||||
object with `"schema_version": 1`:
|
||||
|
||||
- `records` - every `property`, `state` and `event`: its module, file, line and column, its doc
|
||||
comment (the comment lines above it, else the one ending its line), and its fields, each with its
|
||||
type as text, its default as written (or null), its doc, its place and its attributes
|
||||
(`[{"name": "Range", "args": [0, 20.5]}]`);
|
||||
- `registries` - every registry: its record, prefix, resource file, whether it is open, its own
|
||||
attributes, and its entries in their final index order (`{"key": "axe", "constant": "TL_AXE",
|
||||
"index": 0, "file": ..., "line": ..., "col": ..., "fields": [{"name", "value", "file", "line",
|
||||
"col"}]}`), with `contributors`: which resource file or file of `def`s brought which keys in;
|
||||
- `consts` - every const: its type, its value as written, its module and doc;
|
||||
- `functions` - every function a `fn` value can name with no arguments (the states it takes are the
|
||||
runtime's to supply): its return type, `fn_type` and signature.
|
||||
|
||||
Each list is sorted by name (then file and line), a registry's entries are in index order, and the
|
||||
paths are the ones the compiler read, so two runs over the same source write the same file. The
|
||||
engine's runtime is left out.
|
||||
|
||||
### Default parameters, and calls that name what they change
|
||||
|
||||
A parameter can have a default, and a call leaves out what it does not change - the last ones when
|
||||
|
|
@ -2055,6 +2098,8 @@ ludic test tests/math.ludic --test adds # just the test named "adds" (-v: every
|
|||
ludic test -j 4 # four tests at once (default: one per CPU)
|
||||
ludic test packages/ludic.base # the test programs under a directory (a package's)
|
||||
ludic build --check # every check a build makes, nothing written
|
||||
ludic build --check --diagnostics=json # the same, every error as a JSON array on stdout
|
||||
ludic schema -o build/schema.json # records, registries, entries and consts, for editors
|
||||
ludic deps # how tangled the modules are, as the compiler resolved them
|
||||
ludic deps --check tests/deps-baseline.txt # fail when a number rose (--baseline FILE writes them)
|
||||
|
||||
|
|
@ -2068,6 +2113,14 @@ parse, the types, `export`, `uses` and layers, ports and binds, registries, and
|
|||
unknown name) - and writes nothing: no IR, no link. On Maroon Lake it takes about four seconds, for
|
||||
iterating on `uses` lines.
|
||||
|
||||
With `--diagnostics=json` (`ludicc --check --diagnostics=json`) nothing goes to stderr: stdout is one
|
||||
JSON array of `{"file", "line", "col", "severity", "message"}` (`[]` when the program is clean; a
|
||||
`col` of 0 is a place known only by its line), and the exit status is 1 when any is an error. Every
|
||||
type error is in it, every `@Ref` naming no registry, and every module rule broken (`export`,
|
||||
`uses`); an error the parser or the code writer cannot go on from - a token it did not expect, an
|
||||
unknown name while lowering, a def into a registry that does not exist - ends the array, and type
|
||||
errors end it before the module rules are looked at.
|
||||
|
||||
`ludic deps` compiles the program (the package's entry, or a file) with the compiler recording every
|
||||
reference its visibility pass resolves - from the module it is written in to the module of what it
|
||||
names - and every assignment to another module's global. It prints five numbers: `modules` (the
|
||||
|
|
|
|||
14
changes/editor-schema.md
Normal file
14
changes/editor-schema.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**A schema for editors, and every error as JSON.** `ludicc --emit-schema out.json` (and `ludic
|
||||
schema [file] [-o FILE]`) writes what the compiler resolved once the program type-checks: every
|
||||
record with its fields' types, defaults, doc comments and places; every registry with its record,
|
||||
prefix, resource file, openness and 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 brought
|
||||
which entries in); every const; and the functions a `fn` value can name with no arguments.
|
||||
Deterministic, `"schema_version": 1`. Fields and registries carry editor attributes on the existing
|
||||
`@` syntax - `@Ref(Registry)`, `@OneOf(PREFIX_)`, `@Range(lo, hi)`, `@Unit("m/s")`, `@Asset("gltf")`,
|
||||
`@Color`, and `@AppendOnly` / `@ByKey` on a registry - which change nothing but go into the schema;
|
||||
`@Ref` naming no registry is an error. A field may now carry several attributes. `ludicc --check
|
||||
--diagnostics=json` (`ludic build --check --diagnostics=json`) prints every error as one JSON array
|
||||
of `{file, line, col, severity, message}` on stdout; tokens and nodes now know their column.
|
||||
38
examples/lang/attributes.ludic
Normal file
38
examples/lang/attributes.ludic
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# attributes.ludic — editor metadata on the @-syntax: which registry a field indexes, which constants
|
||||
# it takes, its range, its unit, the kind of file it names, that it is a colour, and a registry's own
|
||||
# rules (append only, saved by key). None of them changes what the program does: `ludicc
|
||||
# --emit-schema` carries them to an editor with every record, registry entry and const. One is
|
||||
# checked - @Ref must name a registry that exists.
|
||||
#
|
||||
# Running it prints: 2 1 Lantern 2
|
||||
program Attributes {
|
||||
# a shop in the valley
|
||||
property Vendor {
|
||||
key: string = ""
|
||||
name: string = "" # what the sign over the counter says
|
||||
}
|
||||
@AppendOnly @ByKey
|
||||
registry Vendors of Vendor as VD
|
||||
def Vendors store { name: "General store" }
|
||||
def Vendors marina { name: "Marina" }
|
||||
|
||||
# a tool a camp keeps
|
||||
property Tool {
|
||||
key: string = ""
|
||||
name: string = ""
|
||||
@Ref(Vendors) seller: int = 0 # who sells it
|
||||
@Range(0, 20.5) @Unit("kg") weight: float = 1.0
|
||||
@Asset("gltf")
|
||||
model: string = "" # its model, under assets/kit
|
||||
@Color tint: int = 16777215
|
||||
@OneOf(VD_) stocked_at: int = VD_STORE
|
||||
}
|
||||
@AppendOnly
|
||||
registry Tools of Tool as TL from "data/attr_tools.lres"
|
||||
const CAMP_TOOLS: int = 2 * VD_COUNT # how many a camp can carry
|
||||
# the tool a new camp starts with
|
||||
function first_tool() -> string { return Tools[0].name }
|
||||
entry {
|
||||
print(`{TL_COUNT} {Tools[TL_LANTERN].seller} {Tools[TL_LANTERN].name} {CAMP_TOOLS / 2}`)
|
||||
}
|
||||
}
|
||||
8
examples/lang/data/attr_tools.lres
Normal file
8
examples/lang/data/attr_tools.lres
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# attr_tools.lres - the tools attributes.ludic's registry reads when it is compiled
|
||||
axe { name: "Axe", seller: VD_STORE, weight: 1.5, model: "kit/axe.gltf" }
|
||||
lantern {
|
||||
name: "Lantern"
|
||||
seller: VD_MARINA
|
||||
weight: 0.75
|
||||
stocked_at: VD_MARINA
|
||||
}
|
||||
10
examples/rejected/ref_unknown.ludic
Normal file
10
examples/rejected/ref_unknown.ludic
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# attributes: @Ref names a registry - there is no registry Vendorz, nor Shops (both are reported)
|
||||
program R {
|
||||
property Vendor { key: string = "" }
|
||||
registry Vendors of Vendor
|
||||
property Tool {
|
||||
@Ref(Vendorz) seller: int = 0
|
||||
@Ref(Shops) shop: int = 0
|
||||
}
|
||||
entry { print(VENDORS_COUNT) }
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
589
selfhost/backend/emit_schema.ludic
Normal file
589
selfhost/backend/emit_schema.ludic
Normal 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, "}")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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` }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
113670
selfhost/ludicc.seed.ll
113670
selfhost/ludicc.seed.ll
File diff suppressed because one or more lines are too long
118341
selfhost/ludicc.win.seed.ll
118341
selfhost/ludicc.win.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ program Ludic {
|
|||
import "bundle.ludic"
|
||||
import "scripts.ludic"
|
||||
import "deps.ludic"
|
||||
import "schema.ludic"
|
||||
import "testpar.ludic"
|
||||
import "migrate.ludic"
|
||||
|
||||
|
|
@ -45,10 +46,12 @@ program Ludic {
|
|||
print(" build [file] [--headless] [-o out]")
|
||||
print(" compile to build/<name> (a native binary, nothing to ship beside it)")
|
||||
print(" build [file] --check only check it: types, modules, uses and ports; nothing emitted")
|
||||
print(" build [file] --check --diagnostics=json every error as a JSON array on stdout")
|
||||
print(" test [file|dir...] [-j N] [-v] [--test NAME]")
|
||||
print(" compile and run the project's tests (-v: every test's line)")
|
||||
print(" deps [file] [--graph|--dot|--writes|--uses MOD|--check F|--baseline F]")
|
||||
print(" the module graph as the compiler sees it, and how tangled it is")
|
||||
print(" schema [file] [-o FILE] records, registries and their entries, consts, as JSON (for editors)")
|
||||
print(" migrate state [file] [--prune] [--runtime] module-level vars into states, passed as parameters (0.S); --prune drops states nothing uses, --tighten also mut nothing writes")
|
||||
print(" clean remove build/")
|
||||
print("")
|
||||
|
|
@ -102,6 +105,7 @@ program Ludic {
|
|||
if (cmd == "run") { return cmd_run() }
|
||||
if (cmd == "test") { return cmd_test() }
|
||||
if (cmd == "deps") { return cmd_deps() }
|
||||
if (cmd == "schema") { return cmd_schema() }
|
||||
if (cmd == "migrate") { return cmd_migrate() }
|
||||
if (cmd == "clean") { return cmd_clean() }
|
||||
if (cmd == "fmt") { return cmd_fmt() }
|
||||
|
|
|
|||
|
|
@ -277,6 +277,7 @@ function parse_build_args(start: int) -> pointer {
|
|||
else if a == "--save-temps" { g_save = true }
|
||||
else if a == "--unsafe" { g_unsafe_build = true }
|
||||
else if a == "--check" { g_check_build = true }
|
||||
else if a == "--diagnostics=json" { g_check_build = true; g_diag_json_build = true } # every error, as JSON on stdout
|
||||
else if a == "-o" {
|
||||
ai += 1
|
||||
if ai < arg_count() { g_out = arg(ai) }
|
||||
|
|
@ -284,7 +285,7 @@ function parse_build_args(start: int) -> pointer {
|
|||
}
|
||||
else if a[0] == '-' {
|
||||
err(`ludic: unknown option {a}\n`)
|
||||
err(" build/run take: [file] [--headless|--windowed] [-o out] [--save-temps] [--check]\n")
|
||||
err(" build/run take: [file] [--headless|--windowed] [-o out] [--save-temps] [--check [--diagnostics=json]]\n")
|
||||
g_argerr = true
|
||||
}
|
||||
else { src = a }
|
||||
|
|
@ -307,8 +308,13 @@ function output_path(entry: pointer) -> pointer {
|
|||
# --check: parse, types, modules, uses, ports - everything the compiler checks before it emits -
|
||||
# and nothing emitted or linked; a fraction of a build's time, for iterating on uses lines
|
||||
var g_check_build: bool = false
|
||||
var g_diag_json_build: bool = false # --diagnostics=json: the compiler's JSON array is the whole stdout
|
||||
function check_app(src: pointer) -> int {
|
||||
ensure_ludicc()
|
||||
if g_diag_json_build {
|
||||
if not shq(`{ludicc()} --check --diagnostics=json{title_flag()}{unsafe_flag()} {src}`) { return 1 }
|
||||
return 0
|
||||
}
|
||||
if not shq(`{ludicc()} --check{title_flag()}{unsafe_flag()} {src}`) { return 1 }
|
||||
print(`checked {src}`)
|
||||
return 0
|
||||
|
|
|
|||
48
tools/ludic-cli/schema.ludic
Normal file
48
tools/ludic-cli/schema.ludic
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# ---- ludic schema -------------------------------------------------------------
|
||||
# What an editor of a program's data needs, from the compiler's own view: every record with its
|
||||
# fields, types, defaults, docs and attributes (@Ref, @Range, ...), every registry with its entries
|
||||
# in their final order and where each is written, every const, and the functions a `fn` value can
|
||||
# name. The compiler writes it (`ludicc --emit-schema`, selfhost/backend/emit_schema.ludic) once the
|
||||
# program has type-checked.
|
||||
#
|
||||
# ludic schema [file] the JSON on stdout
|
||||
# ludic schema [file] -o FILE into FILE
|
||||
# ludic schema --unsafe ... a program whose own files write `unsafe`
|
||||
function cmd_schema() -> int {
|
||||
var src = ""
|
||||
var dest = ""
|
||||
var ai = 2
|
||||
while ai < arg_count() {
|
||||
let a = arg(ai)
|
||||
if a == "-o" or a == "--out" {
|
||||
if ai + 1 >= arg_count() { err(`ludic schema: {a} needs a file\n`); return 2 }
|
||||
ai += 1
|
||||
dest = arg(ai)
|
||||
}
|
||||
else if a == "--unsafe" { g_unsafe_build = true }
|
||||
else if a[0] == '-' {
|
||||
err(`ludic schema: unknown option {a}\n`)
|
||||
err(" usage: ludic schema [file] [-o FILE] [--unsafe]\n")
|
||||
return 2
|
||||
}
|
||||
else { src = a }
|
||||
ai += 1
|
||||
}
|
||||
let entry = find_entry(src)
|
||||
if entry == "" { return no_entry() }
|
||||
ensure_ludicc()
|
||||
var target = dest
|
||||
if target == "" { target = tmp_path("schema.json") }
|
||||
let errs = tmp_path("schema.err")
|
||||
if not shq(`{ludicc()} --headless{unsafe_flag()} {sh_single(entry)} --emit-schema {sh_single(target)} 2> {errs}`) {
|
||||
err(`ludic schema: {entry} did not compile:\n`)
|
||||
err(capture(`grep -i error {errs} | head -20`))
|
||||
return 1
|
||||
}
|
||||
if dest == "" {
|
||||
let s = read_file(target)
|
||||
if s == null { err("ludic schema: the compiler wrote no schema (is it older than ludic schema?)\n"); return 1 }
|
||||
out(s)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
# the built-in commands a hook can wrap (every user command that does work)
|
||||
function is_hookable(cmd: pointer) -> bool {
|
||||
return (cmd == "build") or (cmd == "run") or (cmd == "test") or (cmd == "deps") or (cmd == "migrate") or (cmd == "bundle") or (cmd == "pack") or (cmd == "clean") or (cmd == "fmt") or (cmd == "get") or (cmd == "add") or (cmd == "update") or (cmd == "verify") or (cmd == "vendor") or (cmd == "assets") or (cmd == "build-lib")
|
||||
return (cmd == "build") or (cmd == "run") or (cmd == "test") or (cmd == "deps") or (cmd == "schema") or (cmd == "migrate") or (cmd == "bundle") or (cmd == "pack") or (cmd == "clean") or (cmd == "fmt") or (cmd == "get") or (cmd == "add") or (cmd == "update") or (cmd == "verify") or (cmd == "vendor") or (cmd == "assets") or (cmd == "build-lib")
|
||||
}
|
||||
|
||||
# the directory this `ludic` binary lives in, with no trailing slash
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/support/io.ludic")
|
||||
push(f, "selfhost/frontend/ast.ludic")
|
||||
push(f, "selfhost/frontend/lex.ludic")
|
||||
push(f, "selfhost/frontend/diag.ludic")
|
||||
push(f, "selfhost/frontend/parse.ludic")
|
||||
push(f, "selfhost/frontend/parse_game.ludic")
|
||||
push(f, "selfhost/frontend/generics.ludic")
|
||||
|
|
@ -31,6 +32,7 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/frontend/registry.ludic")
|
||||
push(f, "selfhost/frontend/registry_finish.ludic")
|
||||
push(f, "selfhost/frontend/registry_open.ludic")
|
||||
push(f, "selfhost/frontend/attrs.ludic")
|
||||
push(f, "selfhost/frontend/resource.ludic")
|
||||
push(f, "selfhost/frontend/defaults.ludic")
|
||||
push(f, "selfhost/frontend/view.ludic")
|
||||
|
|
@ -86,6 +88,7 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/backend/emit_call.ludic")
|
||||
push(f, "selfhost/backend/emit_fnval.ludic")
|
||||
push(f, "selfhost/backend/emit_vis.ludic")
|
||||
push(f, "selfhost/backend/emit_schema.ludic")
|
||||
push(f, "selfhost/check/check_state.ludic")
|
||||
push(f, "selfhost/check/check_index.ludic")
|
||||
push(f, "selfhost/check/check_types.ludic")
|
||||
|
|
|
|||
|
|
@ -53,6 +53,27 @@ function reject_count(path: pointer, n: int, label: pointer) -> void {
|
|||
if (got == string(n)) { ok(label) } else { bad2(label, `reported {got} error(s), not {n}`) }
|
||||
}
|
||||
|
||||
# --emit-schema: the example's schema holds `want` (a line of its JSON, exactly as written)
|
||||
function schema_case(path: pointer, want: pointer, label: pointer) -> void {
|
||||
let js = `{tmp_dir()}/sc_{flat(path)}.json`
|
||||
if not shq(`bin/ludicc examples/{path}.ludic --emit-schema {js} 2>{tmp_dir()}/sc.err`) {
|
||||
bad2(label, capture_line(`grep -i error {tmp_dir()}/sc.err | head -1`))
|
||||
return
|
||||
}
|
||||
if s_contains(capture(`cat {js}`), want) { ok(label) } else { bad2(label, `the schema has no [{want}]`) }
|
||||
}
|
||||
# --check --diagnostics=json: refused, with exactly `n` diagnostics on stdout, one of them holding `want`
|
||||
function diag_json_case(path: pointer, n: int, want: pointer, label: pointer) -> void {
|
||||
let js = `{tmp_dir()}/dj_{flat(path)}.json`
|
||||
if shq(`bin/ludicc examples/{path}.ludic --check --diagnostics=json > {js} 2>/dev/null`) {
|
||||
bad2(label, "checked clean, and should have been refused")
|
||||
return
|
||||
}
|
||||
let got = capture_line(`grep -c '"severity": "error"' {js}`)
|
||||
if not (got == string(n)) { bad2(label, `reported {got} error(s), not {n}`); return }
|
||||
if s_contains(capture(`cat {js}`), want) { ok(label) } else { bad2(label, `no diagnostic holds [{want}]`) }
|
||||
}
|
||||
|
||||
# a feature example that consumes an in-repo controller package (packages/): same
|
||||
# as feat_case, but the compiler is pointed at packages/ via LUDIC_MODULES so a
|
||||
# `import "ludic.foo/bar.ludic"` resolves, and LUDIC_HOME anchors the engine
|
||||
|
|
@ -1177,6 +1198,8 @@ function cmd_dev_test() -> int {
|
|||
reject_case("rejected/text_plus_number", "text joins text only", "text plus a number is refused")
|
||||
reject_case("rejected/math_into_text", "field a1 of Note wants a string and this is an int", "a Math.* result has its type: an int from Math.max is not text")
|
||||
reject_count("rejected/every_error", 3, "every type error in a program is reported at once, not only the first")
|
||||
diag_json_case("rejected/every_error", 3, "\"line\": 8, \"col\": 15, \"severity\": \"error\", \"message\": \"half takes 1 argument(s) and this call gives 2\"", "--diagnostics=json: every type error as {file, line, col, severity, message}")
|
||||
diag_json_case("rejected/ref_unknown", 2, "@Ref(Shops) names no registry", "--diagnostics=json: every @Ref that names no registry")
|
||||
feat_case("lang/generics", "", "7 Crater Lake a 2 west 7 0.75 2.5 1", "generics.ludic (L5: generic records and functions, nested instances, fn-typed parameters and fields, inference from the result's slot)")
|
||||
reject_case("rejected/generic_unbound", "cannot tell what T is in this call to pool_new", "a call whose type arguments nothing decides is refused")
|
||||
reject_case("rejected/generic_mismatch", "words wants a Pool<string> and this is a Pool<int>", "two instances of one generic are two types")
|
||||
|
|
@ -1207,6 +1230,10 @@ function cmd_dev_test() -> int {
|
|||
reject_case("rejected/alias_twice", "Trail.length is declared twice", "a namespace method is declared once")
|
||||
feat_case("lang/registry", "", "3 Crate true 2 fire_ring -1", "registry.ludic (L8: a registry filled by def declarations - index constants, a count, the key, a lookup)")
|
||||
reject_case("rejected/registry_unknown", "there is no registry Tables", "a def names a registry that exists")
|
||||
feat_case("lang/attributes", "", "2 1 Lantern 2", "attributes.ludic (editor attributes: @Ref, @OneOf, @Range, @Unit, @Asset, @Color, @AppendOnly, @ByKey - no effect on the program)")
|
||||
reject_case("rejected/ref_unknown", "field Tool.seller: @Ref(Vendorz) names no registry", "@Ref names a registry that exists")
|
||||
schema_case("lang/attributes", "{\"name\": \"weight\", \"type\": \"float\", \"default\": \"1.0\", \"file\": \"examples/lang/attributes.ludic\", \"line\": 24, \"col\": 33, \"attributes\": [{\"name\": \"Range\", \"args\": [0, 20.5]}, {\"name\": \"Unit\", \"args\": [\"kg\"]}]", "--emit-schema: a field's type, default, place and attributes")
|
||||
schema_case("lang/attributes", "{\"key\": \"lantern\", \"constant\": \"TL_LANTERN\", \"index\": 1, \"file\": \"examples/lang/data/attr_tools.lres\", \"line\": 3, \"col\": 1", "--emit-schema: a resource entry's constant, index and place")
|
||||
reject_case("rejected/registry_twice", "def Furnishings crate is declared twice", "a registry's key is declared once")
|
||||
reject_case("rejected/registry_field", "Furnishing has no field colour", "a def's fields are the record's")
|
||||
feat_case("modules/open_registry", "", "clock animals weather / 2 2 3", "open_registry.ludic (L8+L3: an open registry takes other modules' defs; its own entries first, then by module name)")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue