Merge commit '863b971' into lang/foundations

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 18:08:12 +03:00
commit e83802f15e
29 changed files with 126663 additions and 106591 deletions

View file

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

View file

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

View 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}`)
}
}

View 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
}

View 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) }
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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() }

View file

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

View 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
}

View file

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

View file

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

View file

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