schema: ludicc --emit-schema / ludic schema, --check --diagnostics=json, and editor attributes

--emit-schema FILE writes the compiler's resolved view once the program type-checks: every
record (fields, types, defaults as written, docs, places, attributes), every registry with its
entries in their final order after the open-registry merge (key, constant, index, file:line:col
of the entry and of each field value, and which file contributed which keys), every const, and
the zero-argument functions a fn value can name. Deterministic, schema_version 1; the runtime is
left out. `ludic schema [file] [-o FILE]` wraps it.

--check --diagnostics=json prints every error as one JSON array on stdout: the checker's and the
module rules' all, a parse or lowering error as the last. Tokens and nodes now carry a column.

Fields take several @attributes; @Ref(Registry), @OneOf(PREFIX_), @Range(lo, hi), @Unit("..."),
@Asset("..."), @Color on a field and @AppendOnly / @ByKey on a registry change nothing but go into
the schema, and @Ref naming no registry is an error (every one reported). Fixtures:
examples/lang/attributes.ludic, examples/rejected/ref_unknown.ludic, cases in ludic-dev test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 18:06:54 +03:00
parent f8d811aa97
commit 42deb76c28
27 changed files with 1220 additions and 23 deletions

View file

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