Merge lang/foundations (9391695) into lang/schemacode: LANGUAGE.md's command list keeps --stdin-file (R10), the code map in schema (R7) and ludic syntax (R8); selfhost/main.ludic carries all three flags; reseeded from the merged source, bootstrap-cfree fixpoint holds (298653 lines), syntax --check clean, no conflict markers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
d39a27a904
81 changed files with 114027 additions and 104730 deletions
|
|
@ -484,19 +484,7 @@ function grammar_alt(root: JVal, node: pointer, marker: pointer) -> []pointer {
|
|||
return out
|
||||
}
|
||||
|
||||
# keywords the self-host parser dispatches on: is_id("x") + streq(t.text, "x")
|
||||
function parser_keywords() -> []pointer {
|
||||
let out = new []pointer
|
||||
add_parser_kw(out, "selfhost/frontend/parse.ludic")
|
||||
add_parser_kw(out, "selfhost/frontend/parse_game.ludic")
|
||||
return out
|
||||
}
|
||||
function add_parser_kw(out: []pointer, path: pointer) -> void {
|
||||
let t = read_file(path)
|
||||
if t == null { return }
|
||||
add_lower(out, collect_after(t, "is_id(" + dq()))
|
||||
add_lower(out, collect_after(t, "streq(t.text, " + dq()))
|
||||
}
|
||||
# the lower-case names among `src` into `out` (a set)
|
||||
function add_lower(out: []pointer, src: []pointer) -> void {
|
||||
var i = 0
|
||||
while i < len(src) { if all_lower(src[i]) { set_add(out, src[i]) }; i += 1 }
|
||||
|
|
@ -537,7 +525,6 @@ function cmd_check_vocab() -> int {
|
|||
let h_widgets = table_set(h, "LUDIC_WIDGETS")
|
||||
let h_builtins = c_table_names(h, "LUDIC_BUILTINS")
|
||||
let h_intrinsics = c_table_names(h, "LUDIC_INTRINSICS")
|
||||
let h_reserved = table_set(h, "LUDIC_KW_RESERVED")
|
||||
|
||||
# --- against the JetBrains lexer ---
|
||||
let kt = read_file("tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicTokens.kt")
|
||||
|
|
@ -561,16 +548,26 @@ function cmd_check_vocab() -> int {
|
|||
cmp_sets("primitive types", h_types, grammar_alt(g, "keyword", "fixed"), "ludic.tmLanguage.json")
|
||||
}
|
||||
|
||||
# --- against the self-host parser ---
|
||||
let pkw = parser_keywords()
|
||||
if len(pkw) == 0 {
|
||||
cv_problem("could not extract any keywords from selfhost/frontend/parse*.ludic")
|
||||
} else {
|
||||
cmp_unparsed("declaration keywords", h_decl, pkw, h_reserved)
|
||||
cmp_unparsed("clause keywords", h_clause, pkw, h_reserved)
|
||||
# reserved words the parser now accepts should be promoted
|
||||
var i = 0
|
||||
while i < len(h_reserved) { if set_has(pkw, h_reserved[i]) { cv_problem("LUDIC_KW_RESERVED lists a keyword the parser now accepts — promote it: " + h_reserved[i]) }; i += 1 }
|
||||
# --- the keyword, type and phase tables, the docs and the parser, against the compiler's own
|
||||
# vocabulary (ludicc --emit-syntax; syntax_gen.ludic) ---
|
||||
if sx_vocab() == null { cv_problem("bin/ludicc has no --emit-syntax: rebuild it (ludic-dev build)") }
|
||||
else {
|
||||
g_sx_prob = new []pointer
|
||||
let ts = sx_targets()
|
||||
var ti = 0
|
||||
while ti < len(ts) {
|
||||
let text = read_file(ts[ti])
|
||||
if text != null {
|
||||
let want = sx_regenerate(ts[ti], text)
|
||||
if want != null and not (want == text) { cv_problem(ts[ti] + " is behind the vocabulary - run ludic-dev syntax") }
|
||||
}
|
||||
sx_carries(ts[ti])
|
||||
ti += 1
|
||||
}
|
||||
sx_docs()
|
||||
sx_parser()
|
||||
var pi2 = 0
|
||||
while pi2 < len(g_sx_prob) { cv_problem(g_sx_prob[pi2]); pi2 += 1 }
|
||||
}
|
||||
|
||||
if CV_N > 0 {
|
||||
|
|
@ -581,16 +578,6 @@ function cmd_check_vocab() -> int {
|
|||
return 0
|
||||
}
|
||||
|
||||
# keywords in `kws` the parser never dispatches on (minus reserved) are a problem
|
||||
function cmp_unparsed(label: pointer, kws: []pointer, pkw: []pointer, reserved: []pointer) -> void {
|
||||
var i = 0
|
||||
while i < len(kws) {
|
||||
if not set_has(pkw, kws[i]) {
|
||||
if not set_has(reserved, kws[i]) { cv_problem("ludic_syntax.h lists a " + label + " the selfhost parser never dispatches on: " + kws[i]) }
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# json/xml asset validation — replaces the python3 json.load / xml.dom checks
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ program LudicDev {
|
|||
import "lsp_test.ludic"
|
||||
import "forgejo.ludic"
|
||||
import "checks.ludic"
|
||||
import "syntax_gen.ludic"
|
||||
import "docgen.ludic"
|
||||
import "docgen_gen.ludic"
|
||||
import "docgen_check.ludic"
|
||||
|
|
@ -67,6 +68,7 @@ program LudicDev {
|
|||
print(" check-docs every ```ludic doc fence parses (or is marked skip/expect-error)")
|
||||
print(" check-impl every implemented feature has a docs/language page")
|
||||
print(" check-vocabulary the vocabulary is in sync across grammar / lexer / header / parser")
|
||||
print(" syntax [--check] write every grammar's keyword, type and phase lists from ludicc --emit-syntax")
|
||||
print(" lint-asset <file> validate one editor .json / .xml asset")
|
||||
print(" docs-gen [--out DIR] generate the documentation site (default build/pages)")
|
||||
print(" docs-check [DIR] coverage/integrity guard over a generated docs site")
|
||||
|
|
@ -108,6 +110,7 @@ program LudicDev {
|
|||
if (cmd == "check-docs") { return cmd_check_docs() }
|
||||
if (cmd == "check-impl") { return cmd_check_impl() }
|
||||
if (cmd == "check-vocabulary") { return cmd_check_vocab() }
|
||||
if (cmd == "syntax") { return cmd_syntax_gen() }
|
||||
if (cmd == "lint-asset") { return cmd_lint_asset() }
|
||||
if (cmd == "docs-palette") { return cmd_docs_palette() }
|
||||
if (cmd == "glgen") { return cmd_glgen() }
|
||||
|
|
|
|||
|
|
@ -286,6 +286,20 @@ function cmd_test_lsp() -> int {
|
|||
lsp_notify("textDocument/didSave", `{{"textDocument":{lsp_doc(ccf)}}}`)
|
||||
lsp_request("textDocument/documentSymbol", `{{"textDocument":{lsp_doc(ccf)}}}`)
|
||||
|
||||
# ---- the declarations a module file holds (R8): exported records, a registry with its
|
||||
# attributes, states, events, actions, ports, enums, reducers and components, every member
|
||||
let dsrc = "module kits uses base\n\n# a kit\nexport property Kit {\n key: string = \"\"\n @Asset(\"gltf\") model: string = \"\"\n @Node(model) grip: string = \"\"\n @Ref(Kits) spare: int = 0\n pick: fn(int, float) -> bool\n}\n\n# every kit\n@AppendOnly @ByKey\nexport registry Kits of Kit as KIT from \"kits.lres\"\nexport state KitState {\n held: int = -1\n seen: []int\n}\nexport event KitDropped { kit: int, x: float }\nexport action PickKit { kit: int }\nexport port KitWorld {\n ground: fn(float, float) -> float\n}\nenum Grade { Low, High }\nreducer KitState on PickKit(st: mut KitState, a: PickKit) {\n st.held = a.kit\n}\ncomponent Tray(k: KitState) {\n prop step: int = 1\n state open: bool = false\n on press() { }\n}\nexport function kit_weight(k: Kit) -> int { return k.spare }\n"
|
||||
let dfile = tmp_path("lsp_decls.ludic")
|
||||
write_file(dfile, dsrc)
|
||||
lsp_open(dfile, "ludic")
|
||||
let duri = lsp_doc(dfile)
|
||||
let dsyms = lsp_request("textDocument/documentSymbol", `{{"textDocument":{duri}}}`)
|
||||
let dref = lsp_request("textDocument/definition", `{{"textDocument":{duri},"position":{position_of(dsrc, "@Ref(Kits)", 6)}}}`)
|
||||
let dhov = lsp_request("textDocument/hover", `{{"textDocument":{duri},"position":{position_of(dsrc, "@Ref(Kits)", 6)}}}`)
|
||||
let dnode = lsp_request("textDocument/definition", `{{"textDocument":{duri},"position":{position_of(dsrc, "@Node(model)", 7)}}}`)
|
||||
let dty = lsp_request("textDocument/definition", `{{"textDocument":{duri},"position":{position_of(dsrc, "kit_weight(k: Kit)", 14)}}}`)
|
||||
let dat = lsp_request("textDocument/hover", `{{"textDocument":{duri},"position":{position_of(dsrc, "@Ref(Kits)", 1)}}}`)
|
||||
|
||||
# ---- workspace-wide symbol search ---------------------------------------
|
||||
let ws = lsp_request("workspace/symbol", `{{"query":"Stats"}}`)
|
||||
|
||||
|
|
@ -456,5 +470,43 @@ function cmd_test_lsp() -> int {
|
|||
check_true("a name inside a template hole resolves", Text.contains(j_get(j_get(lsp_result(thov), "contents"), "value").s, "param variant: int"))
|
||||
check("find-usages reaches into template holes", loc_lines_key(lsp_result(trefs)), "1,3,4")
|
||||
check_true("positions after a multi-line template stay on the right line", Text.contains(j_get(j_get(lsp_result(tafter), "contents"), "value").s, "let multi"))
|
||||
# a module file's declarations, each by its own name and kind, with every member as a child
|
||||
let dk = lsp_result(dsyms)
|
||||
check_true("an exported property is a struct named for itself, all its fields children", lsp_sym_is(dk, "Kit", 23, 5))
|
||||
check_true("a registry is a symbol of its own, with what it holds", lsp_sym_is(dk, "Kits", 18, 0) and Text.contains(lsp_sym_detail(dk, "Kits"), "of Kit as KIT"))
|
||||
check_true("a state is a struct with its fields", lsp_sym_is(dk, "KitState", 23, 2))
|
||||
check_true("an event and an action are events", lsp_sym_is(dk, "KitDropped", 24, 2) and lsp_sym_is(dk, "PickKit", 24, 1))
|
||||
check_true("a port is an interface, a fn-typed member written out", lsp_sym_is(dk, "KitWorld", 11, 1) and Text.contains(lsp_sym_detail(dk, "KitWorld"), "fn(float, float) -> float"))
|
||||
check_true("an enum and its members", lsp_sym_is(dk, "Grade", 10, 2))
|
||||
check_true("a reducer is named for its state and its action", lsp_sym_is(dk, "KitState on PickKit", 12, -1))
|
||||
check_true("a component holds its props, state and events", lsp_sym_is(dk, "Tray", 5, 3))
|
||||
check_true("an exported function is still a function", lsp_sym_is(dk, "kit_weight", 12, -1))
|
||||
check_true("go-to-definition on @Ref(Kits) reaches the registry", j_get(j_get(j_get(lsp_result(dref), "range"), "start"), "line").num == 13)
|
||||
check_true("hover on @Ref(Kits) shows the registry", Text.contains(j_get(j_get(lsp_result(dhov), "contents"), "value").s, "registry Kits of Kit"))
|
||||
check_true("go-to-definition on @Node(model) reaches the field it names", j_get(j_get(j_get(lsp_result(dnode), "range"), "start"), "line").num == 5)
|
||||
check_true("go-to-definition on an exported record's type name", j_get(j_get(j_get(lsp_result(dty), "range"), "start"), "line").num == 3)
|
||||
check_true("hover on an attribute shows its reference page", Text.contains(j_get(j_get(lsp_result(dat), "contents"), "value").s, "@Ref(Registry)"))
|
||||
return report()
|
||||
}
|
||||
|
||||
# the symbol named `name` among `syms` has LSP kind `kind` and, unless n is -1, n children
|
||||
function lsp_sym_is(syms: JVal, name: pointer, kind: int, n: int) -> bool {
|
||||
var i = 0
|
||||
while i < len(syms.kids) {
|
||||
let y = syms.kids[i]
|
||||
if j_get(y, "name").s == name {
|
||||
if j_get(y, "kind").num != kind { return false }
|
||||
return n < 0 or len(j_get(y, "children").kids) == n
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
function lsp_sym_detail(syms: JVal, name: pointer) -> pointer {
|
||||
var i = 0
|
||||
while i < len(syms.kids) {
|
||||
if j_get(syms.kids[i], "name").s == name { return j_get(syms.kids[i], "detail").s }
|
||||
i += 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ program Ludic {
|
|||
import "scripts.ludic"
|
||||
import "deps.ludic"
|
||||
import "schema.ludic"
|
||||
import "syntax_cli.ludic"
|
||||
import "ui_preview.ludic"
|
||||
import "testpar.ludic"
|
||||
import "migrate.ludic"
|
||||
|
|
@ -48,11 +49,13 @@ program Ludic {
|
|||
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(" build [file] --check --stdin-file PATH stdin is PATH's unsaved text (an editor's buffer)")
|
||||
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(" syntax [--json] [-o FILE] the language's vocabulary: keywords, declarations, types, phases, attributes, operators")
|
||||
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("")
|
||||
|
|
@ -108,6 +111,7 @@ program Ludic {
|
|||
if (cmd == "test") { return cmd_test() }
|
||||
if (cmd == "deps") { return cmd_deps() }
|
||||
if (cmd == "schema") { return cmd_schema() }
|
||||
if (cmd == "syntax") { return cmd_syntax() }
|
||||
if (cmd == "migrate") { return cmd_migrate() }
|
||||
if (cmd == "clean") { return cmd_clean() }
|
||||
if (cmd == "fmt") { return cmd_fmt() }
|
||||
|
|
|
|||
|
|
@ -280,6 +280,11 @@ function parse_build_args(start: int) -> pointer {
|
|||
else if a == "--no-maps" { g_no_maps_build = true } # --check leaves the map directories alone
|
||||
else if a == "--maps" { g_maps_build = true } # ... or reads them, whatever program it checks
|
||||
else if a == "--diagnostics=json" { g_check_build = true; g_diag_json_build = true } # every error, as JSON on stdout
|
||||
else if a == "--stdin-file" { # stdin is that file's unsaved text: checked, never built
|
||||
ai += 1
|
||||
if ai < arg_count() { g_stdin_file_build = arg(ai); g_check_build = true }
|
||||
else { err("ludic: --stdin-file needs a path\n"); g_argerr = true }
|
||||
}
|
||||
else if a == "-o" {
|
||||
ai += 1
|
||||
if ai < arg_count() { g_out = arg(ai) }
|
||||
|
|
@ -287,7 +292,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 [--diagnostics=json] [--maps|--no-maps]]\n")
|
||||
err(" build/run take: [file] [--headless|--windowed] [-o out] [--save-temps] [--check [--diagnostics=json] [--stdin-file path] [--maps|--no-maps]]\n")
|
||||
g_argerr = true
|
||||
}
|
||||
else { src = a }
|
||||
|
|
@ -311,6 +316,7 @@ function output_path(entry: pointer) -> pointer {
|
|||
# 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
|
||||
var g_stdin_file_build: pointer = "" # --stdin-file <path>: the compiler reads stdin where it would read <path>
|
||||
var g_no_maps_build: bool = false
|
||||
var g_maps_build: bool = false
|
||||
# the maps are checked with the package's entry - the game - and left alone for any other program: a
|
||||
|
|
@ -330,13 +336,17 @@ function bld_same_path(a: pointer, b: pointer) -> bool {
|
|||
if len(y) > 2 and y[0] == '.' and y[1] == '/' { y = y[2..len(y)] }
|
||||
return x == y
|
||||
}
|
||||
function stdin_file_flag() -> pointer {
|
||||
if g_stdin_file_build == "" { return "" }
|
||||
return ` --stdin-file {sh_single(g_stdin_file_build)}`
|
||||
}
|
||||
function check_app(src: pointer) -> int {
|
||||
ensure_ludicc()
|
||||
if g_diag_json_build {
|
||||
if not shq(`{ludicc()} --check --diagnostics=json{no_maps_flag(src)}{title_flag()}{unsafe_flag()} {src}`) { return 1 }
|
||||
if not shq(`{ludicc()} --check --diagnostics=json{no_maps_flag(src)}{title_flag()}{unsafe_flag()}{stdin_file_flag()} {src}`) { return 1 }
|
||||
return 0
|
||||
}
|
||||
if not shq(`{ludicc()} --check{no_maps_flag(src)}{title_flag()}{unsafe_flag()} {src}`) { return 1 }
|
||||
if not shq(`{ludicc()} --check{no_maps_flag(src)}{title_flag()}{unsafe_flag()}{stdin_file_flag()} {src}`) { return 1 }
|
||||
print(`checked {src}`)
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/frontend/ast.ludic")
|
||||
push(f, "selfhost/frontend/lex.ludic")
|
||||
push(f, "selfhost/frontend/diag.ludic")
|
||||
push(f, "selfhost/frontend/stdin_file.ludic")
|
||||
push(f, "selfhost/frontend/parse.ludic")
|
||||
push(f, "selfhost/frontend/parse_game.ludic")
|
||||
push(f, "selfhost/frontend/generics.ludic")
|
||||
|
|
@ -34,6 +35,7 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/frontend/registry_open.ludic")
|
||||
push(f, "selfhost/frontend/attrs.ludic")
|
||||
push(f, "selfhost/frontend/attrs_node.ludic")
|
||||
push(f, "selfhost/frontend/vocab.ludic")
|
||||
push(f, "selfhost/frontend/resource.ludic")
|
||||
push(f, "selfhost/frontend/permap.ludic")
|
||||
push(f, "selfhost/frontend/permap_consts.ludic")
|
||||
|
|
|
|||
86
tools/ludic-cli/syntax_cli.ludic
Normal file
86
tools/ludic-cli/syntax_cli.ludic
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# ---- ludic syntax -------------------------------------------------------------
|
||||
# The language's vocabulary as the compiler holds it (`ludicc --emit-syntax`,
|
||||
# selfhost/frontend/vocab.ludic): every keyword with its role and whether it is
|
||||
# reserved, every declaration's form, the built-in types, the phases, every
|
||||
# attribute with where it goes, what it takes and what it means, the operators and
|
||||
# the literal forms. An editor's grammar is written from it (`ludic-dev syntax`).
|
||||
#
|
||||
# ludic syntax one line per entry: kind, name, role
|
||||
# ludic syntax --json the JSON, as the compiler writes it
|
||||
# ludic syntax ... -o FILE into FILE
|
||||
|
||||
# the vocabulary's JSON from the compiler, or null when it cannot say
|
||||
function syntax_json_text() -> pointer {
|
||||
ensure_ludicc()
|
||||
let js = tmp_path("syntax.json")
|
||||
if not shq(`{ludicc()} --emit-syntax > {sh_single(js)} 2> {tmp_path("syntax.err")}`) { return null }
|
||||
let s = read_file(js)
|
||||
if s == null or slen(s) == 0 or s[0] != '{' { return null }
|
||||
return s
|
||||
}
|
||||
|
||||
function cmd_syntax() -> int {
|
||||
var json = false
|
||||
var dest = ""
|
||||
var ai = 2
|
||||
while ai < arg_count() {
|
||||
let a = arg(ai)
|
||||
if a == "--json" { json = true }
|
||||
else if a == "-o" or a == "--out" {
|
||||
if ai + 1 >= arg_count() { err(`ludic syntax: {a} needs a file\n`); return 2 }
|
||||
ai += 1
|
||||
dest = arg(ai)
|
||||
}
|
||||
else {
|
||||
err(`ludic syntax: unknown argument {a}\n`)
|
||||
err(" usage: ludic syntax [--json] [-o FILE]\n")
|
||||
return 2
|
||||
}
|
||||
ai += 1
|
||||
}
|
||||
let s = syntax_json_text()
|
||||
if s == null { err("ludic syntax: the compiler wrote no vocabulary (is it older than ludic syntax?)\n"); return 1 }
|
||||
var text = s
|
||||
if not json { text = syntax_lines(s) }
|
||||
if dest == "" { out(text); return 0 }
|
||||
if not write_file(dest, text) { err(`ludic syntax: cannot write {dest}\n`); return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
# the plain listing, `keyword module declaration`, one entry a line - read off the JSON's own
|
||||
# layout (a list opens on ` "name": [`, one entry per line after it) rather than parsed
|
||||
function syntax_lines(s: pointer) -> pointer {
|
||||
var o = ""
|
||||
var kind = ""
|
||||
let n = slen(s)
|
||||
var i = 0
|
||||
while i < n {
|
||||
let ln = line_at(s, i)
|
||||
i = i + slen(ln) + 1
|
||||
if s_starts(ln, " \"") and s_contains(ln, "[") {
|
||||
kind = sslice(ln, 3, s_index(ln, "\"", 3) - 1) # "keywords" -> keyword
|
||||
continue
|
||||
}
|
||||
if not s_starts(ln, " {\"") { continue }
|
||||
var name = syntax_field(ln, s_index(ln, "\": ", 0) + 3)
|
||||
if kind == "attribute" { name = "@" + name }
|
||||
var role = ""
|
||||
let r = s_index(ln, "\"role\": ", 0)
|
||||
if r >= 0 { role = syntax_field(ln, r + 8) }
|
||||
let f = s_index(ln, "\"form\": ", 0)
|
||||
if f >= 0 { role = syntax_field(ln, f + 8) }
|
||||
o = o + kind + "\t" + name + "\t" + role + "\n"
|
||||
}
|
||||
return o
|
||||
}
|
||||
# the JSON string starting at ln[q] (its opening quote), unescaped
|
||||
function syntax_field(ln: pointer, q: int) -> pointer {
|
||||
var o = ""
|
||||
var i = q + 1
|
||||
while ln[i] != 0 and ln[i] != '"' {
|
||||
if ln[i] == '\\' { i += 1 }
|
||||
o = o + sslice(ln, i, i + 1)
|
||||
i += 1
|
||||
}
|
||||
return o
|
||||
}
|
||||
508
tools/ludic-cli/syntax_gen.ludic
Normal file
508
tools/ludic-cli/syntax_gen.ludic
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
# syntax_gen.ludic — one vocabulary, written into every editor's grammar and checked everywhere
|
||||
# else (`ludic-dev syntax [--check]`).
|
||||
#
|
||||
# The vocabulary is what `ludicc --emit-syntax` prints (selfhost/frontend/vocab.ludic, held to the
|
||||
# parser's own recognisers). From it, between marked lines, this writes:
|
||||
#
|
||||
# tools/ludic-tools/ludic_syntax.h LUDIC_KW_DECL / _CLAUSE / _STMT, LUDIC_TYPES,
|
||||
# LUDIC_PHASES, LUDIC_ATTRIBUTES
|
||||
# tools/ludic-tools/lsp.ludic, lsp/types.ludic the server's word tests (keyword, type, phase,
|
||||
# the words that may also be names)
|
||||
# tools/editors/shared/ludic.tmLanguage.json every pattern marked "ludic-dev syntax: <group>"
|
||||
# (and its copy in tools/editors/vscode/syntaxes/)
|
||||
# tools/editors/jetbrains/.../LudicTokens.kt LudicVocabulary's keyword, type and phase sets
|
||||
# tools/editors/emacs/ludic-mode.el the keyword, type and phase lists
|
||||
#
|
||||
# and it checks what cannot be written: that docs/language has a page for every keyword, type,
|
||||
# phase and attribute, and that the parser tests no word and reads no attribute the vocabulary
|
||||
# lacks (a scan of selfhost/frontend) - nor the vocabulary lists one the parser never tests.
|
||||
#
|
||||
# ludic-dev syntax rewrite the marked regions from the vocabulary
|
||||
# ludic-dev syntax --check fail, naming each, on a region behind the vocabulary or a gap
|
||||
|
||||
var g_sx: JVal = null
|
||||
var g_sx_prob: []pointer = new []pointer
|
||||
|
||||
# the vocabulary, from bin/ludicc (null when that compiler has no --emit-syntax)
|
||||
function sx_vocab() -> JVal {
|
||||
if g_sx != null { return g_sx }
|
||||
ensure_ludicc()
|
||||
let js = tmp_path("syntax.json")
|
||||
if not shq(`bin/ludicc --emit-syntax > {js} 2> {tmp_path("syntax.err")}`) { return null }
|
||||
let s = read_file(js)
|
||||
if s == null or slen(s) == 0 or s[0] != '{' { return null }
|
||||
g_sx = json_parse(s)
|
||||
return g_sx
|
||||
}
|
||||
|
||||
# the `key` of every entry of `list` whose role is `role` ("" for all)
|
||||
function sx_words(list: pointer, key: pointer, role: pointer) -> []pointer {
|
||||
let out = new []pointer
|
||||
let xs = j_get(g_sx, list)
|
||||
var i = 0
|
||||
while i < len(xs.kids) {
|
||||
let e = xs.kids[i]
|
||||
if slen(role) == 0 or j_get(e, "role").s == role { push(out, j_get(e, key).s) }
|
||||
i += 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
function sx_kw(role: pointer) -> []pointer { return sx_words("keywords", "word", role) }
|
||||
# a statement is written like one, and and/or/not highlight with them
|
||||
function sx_stmt() -> []pointer { return set_union(sx_kw("statement"), sx_kw("operator")) }
|
||||
# every keyword that is not a literal (true, false and null lex as booleans)
|
||||
function sx_all_kw() -> []pointer { return set_union(set_union(sx_kw("declaration"), sx_kw("modifier")), sx_stmt()) }
|
||||
# the words the language server lets stand as a name where one is written (`var view = ...`,
|
||||
# `a.model`): every word the parser does not reserve, and every declaring or modifying word
|
||||
function sx_contextual() -> []pointer {
|
||||
let out = new []pointer
|
||||
let xs = j_get(g_sx, "keywords")
|
||||
var i = 0
|
||||
while i < len(xs.kids) {
|
||||
let e = xs.kids[i]
|
||||
let r = j_get(e, "role").s
|
||||
if (j_get(e, "reserved").b == 0 and r != "constant" and r != "operator") or r == "declaration" or r == "modifier" { push(out, j_get(e, "word").s) }
|
||||
i += 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
function sx_attr_names() -> []pointer {
|
||||
let out = new []pointer
|
||||
let xs = sx_words("attributes", "name", "")
|
||||
var i = 0
|
||||
while i < len(xs) { push(out, "@" + xs[i]); i += 1 }
|
||||
return out
|
||||
}
|
||||
|
||||
# ---- writing a list in each file's own spelling ----
|
||||
# words quoted with `q`, joined by `sep`, after `lead` on the first line and under `indent` on the
|
||||
# rest, wrapped before column `width`; `last` follows the final word (a C table's terminating 0)
|
||||
function sx_wrap(words: []pointer, q: pointer, sep: pointer, lead: pointer, indent: pointer, width: int, last: pointer) -> pointer {
|
||||
var out = lead
|
||||
var col = slen(lead)
|
||||
var fresh = true
|
||||
var i = 0
|
||||
while i < len(words) {
|
||||
var item = q + words[i] + q
|
||||
if i + 1 < len(words) { item = item + s_trim(sep) } else { item = item + last }
|
||||
var gap = ""
|
||||
if not fresh { gap = sep_gap(sep) }
|
||||
if not fresh and col + slen(gap) + slen(item) > width {
|
||||
out = out + "\n" + indent
|
||||
col = slen(indent)
|
||||
gap = ""
|
||||
}
|
||||
out = out + gap + item
|
||||
col = col + slen(gap) + slen(item)
|
||||
fresh = false
|
||||
i += 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
# the spaces a separator puts between two items (", " -> " ", "," -> "")
|
||||
function sep_gap(sep: pointer) -> pointer {
|
||||
if s_ends(sep, " ") { return " " }
|
||||
return ""
|
||||
}
|
||||
function sx_c_table(name: pointer, words: []pointer) -> pointer {
|
||||
return "static const char* " + name + "[] = {\n" + sx_wrap(words, "\"", ",", " ", " ", 100, ", 0") + "\n};\n"
|
||||
}
|
||||
function sx_kt_set(name: pointer, words: []pointer) -> pointer {
|
||||
return " val " + name + " = setOf(\n" + sx_wrap(words, "\"", ", ", " ", " ", 100, "") + "\n )\n"
|
||||
}
|
||||
function sx_el_list(name: pointer, words: []pointer) -> pointer {
|
||||
return "(defconst " + name + "\n" + sx_wrap(words, "\"", " ", " '(", " ", 96, "))") + "\n"
|
||||
}
|
||||
# a Ludic word test: `if (w == "a") or ... { return true }`, six to a line
|
||||
function sx_ludic_test(fname: pointer, words: []pointer, ind: pointer) -> pointer {
|
||||
var out = ind + "function " + fname + "(w: pointer) -> bool {\n"
|
||||
var i = 0
|
||||
while i < len(words) {
|
||||
var line = ind + " if "
|
||||
var k = 0
|
||||
while k < 6 and i < len(words) {
|
||||
if k > 0 { line = line + " or " }
|
||||
line = line + "(w == \"" + words[i] + "\")"
|
||||
k += 1
|
||||
i += 1
|
||||
}
|
||||
out = out + line + " { return true }\n"
|
||||
}
|
||||
return out + ind + " return false\n" + ind + "}\n"
|
||||
}
|
||||
# a TextMate alternation, as it is written inside a JSON string: \\b(a|b|c)\\b
|
||||
function sx_tm_alt(words: []pointer) -> pointer {
|
||||
var out = ""
|
||||
var i = 0
|
||||
while i < len(words) {
|
||||
if i > 0 { out = out + "|" }
|
||||
out = out + words[i]
|
||||
i += 1
|
||||
}
|
||||
return "\\\\b(" + out + ")\\\\b"
|
||||
}
|
||||
|
||||
# ---- the regions ----
|
||||
function sx_begin_note() -> pointer { return "ludic-dev syntax: begin - generated from `ludicc --emit-syntax` (selfhost/frontend/vocab.ludic); run `ludic-dev syntax`, do not edit" }
|
||||
function sx_header_region() -> pointer {
|
||||
var o = "/* " + sx_begin_note() + " */\n"
|
||||
o = o + sx_c_table("LUDIC_KW_DECL", sx_kw("declaration"))
|
||||
o = o + sx_c_table("LUDIC_KW_CLAUSE", sx_kw("modifier"))
|
||||
o = o + sx_c_table("LUDIC_KW_STMT", sx_stmt())
|
||||
o = o + sx_c_table("LUDIC_TYPES", sx_words("types", "name", ""))
|
||||
o = o + sx_c_table("LUDIC_PHASES", sx_words("phases", "name", ""))
|
||||
o = o + sx_c_table("LUDIC_ATTRIBUTES", sx_words("attributes", "name", ""))
|
||||
return o + "/* ludic-dev syntax: end */\n"
|
||||
}
|
||||
function sx_kotlin_region() -> pointer {
|
||||
var o = " // " + sx_begin_note() + "\n"
|
||||
o = o + sx_kt_set("DECL", sx_kw("declaration"))
|
||||
o = o + sx_kt_set("CLAUSE", sx_kw("modifier"))
|
||||
o = o + sx_kt_set("STMT", sx_stmt())
|
||||
o = o + sx_kt_set("PRIMITIVES", sx_words("types", "name", ""))
|
||||
o = o + sx_kt_set("PHASES", sx_words("phases", "name", ""))
|
||||
return o + " // ludic-dev syntax: end\n"
|
||||
}
|
||||
function sx_emacs_region() -> pointer {
|
||||
var o = ";; " + sx_begin_note() + "\n"
|
||||
o = o + sx_el_list("ludic--declaration-keywords", sx_kw("declaration"))
|
||||
o = o + sx_el_list("ludic--clause-keywords", sx_kw("modifier"))
|
||||
o = o + sx_el_list("ludic--statement-keywords", sx_stmt())
|
||||
o = o + sx_el_list("ludic--types", sx_words("types", "name", ""))
|
||||
o = o + sx_el_list("ludic--phases", sx_words("phases", "name", ""))
|
||||
return o + ";; ludic-dev syntax: end\n"
|
||||
}
|
||||
function sx_lsp_region() -> pointer {
|
||||
var o = " # " + sx_begin_note() + "\n"
|
||||
o = o + sx_ludic_test("is_type_word", sx_words("types", "name", ""), " ")
|
||||
o = o + sx_ludic_test("is_phase_word", sx_words("phases", "name", ""), " ")
|
||||
o = o + sx_ludic_test("is_keyword_word", sx_all_kw(), " ")
|
||||
return o + " # ludic-dev syntax: end\n"
|
||||
}
|
||||
function sx_lsp_types_region() -> pointer {
|
||||
return "# " + sx_begin_note() + "\n" + sx_ludic_test("is_contextual_word", sx_contextual(), "") + "# ludic-dev syntax: end\n"
|
||||
}
|
||||
|
||||
# `text` with the lines from the one holding "ludic-dev syntax: begin" to the one holding
|
||||
# "ludic-dev syntax: end" replaced by `region`; null when the markers are not there
|
||||
function sx_splice(text: pointer, region: pointer) -> pointer {
|
||||
let b = s_index(text, "ludic-dev syntax: begin", 0)
|
||||
if b < 0 { return null }
|
||||
let e = s_index(text, "ludic-dev syntax: end", b)
|
||||
if e < 0 { return null }
|
||||
var ls = b
|
||||
while ls > 0 and text[ls - 1] != '\n' { ls -= 1 }
|
||||
var le = e
|
||||
while text[le] != 0 and text[le] != '\n' { le += 1 }
|
||||
if text[le] == '\n' { le += 1 }
|
||||
return sslice(text, 0, ls) + region + sslice(text, le, slen(text))
|
||||
}
|
||||
|
||||
# the TextMate grammar: each pattern whose line says "comment": "ludic-dev syntax: <group>" has its
|
||||
# "match" on that line rewritten
|
||||
function sx_tm_group(group: pointer) -> pointer {
|
||||
if group == "declaration" { return sx_tm_alt(sx_kw("declaration")) }
|
||||
if group == "modifier" { return sx_tm_alt(sx_kw("modifier")) }
|
||||
if group == "statement" { return sx_tm_alt(sx_kw("statement")) }
|
||||
if group == "operator" { return sx_tm_alt(sx_kw("operator")) }
|
||||
if group == "constant" { return sx_tm_alt(sx_kw("constant")) }
|
||||
if group == "types" { return sx_tm_alt(sx_words("types", "name", "")) }
|
||||
if group == "phases" { return sx_tm_alt(sx_words("phases", "name", "")) }
|
||||
if group == "phase-clause" {
|
||||
let alt = sx_tm_alt(sx_words("phases", "name", ""))
|
||||
return "\\\\b(phase)\\\\s+" + sslice(alt, 3, slen(alt))
|
||||
}
|
||||
return null
|
||||
}
|
||||
function sx_tm_rewrite(text: pointer) -> pointer {
|
||||
let marker = "\"comment\": \"ludic-dev syntax: "
|
||||
var out = ""
|
||||
var i = 0
|
||||
let n = slen(text)
|
||||
while i < n {
|
||||
let ln = line_at(text, i)
|
||||
i = i + slen(ln) + 1
|
||||
var line = ln
|
||||
let m = s_index(ln, marker, 0)
|
||||
if m >= 0 {
|
||||
let g0 = m + slen(marker)
|
||||
let group = sslice(ln, g0, s_index(ln, "\"", g0))
|
||||
let alt = sx_tm_group(group)
|
||||
let mq = s_index(ln, "\"match\": \"", 0)
|
||||
if alt == null or mq < 0 { push(g_sx_prob, `ludic.tmLanguage.json: an unknown group "{group}" or no "match" beside it`) }
|
||||
else {
|
||||
let a = mq + 10
|
||||
var e = a
|
||||
while ln[e] != 0 and ln[e] != '"' { if ln[e] == '\\' { e += 1 }; e += 1 }
|
||||
line = sslice(ln, 0, a) + alt + sslice(ln, e, slen(ln))
|
||||
}
|
||||
}
|
||||
out = out + line
|
||||
if i <= n { out = out + "\n" }
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
# the files and what each should hold; kind: "region" (between the markers) or "tm"
|
||||
function sx_targets() -> []pointer {
|
||||
let t = new []pointer
|
||||
push(t, "tools/ludic-tools/ludic_syntax.h")
|
||||
push(t, "tools/ludic-tools/lsp.ludic")
|
||||
push(t, "tools/ludic-tools/lsp/types.ludic")
|
||||
push(t, "tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicTokens.kt")
|
||||
push(t, "tools/editors/emacs/ludic-mode.el")
|
||||
push(t, "tools/editors/shared/ludic.tmLanguage.json")
|
||||
push(t, "tools/editors/vscode/syntaxes/ludic.tmLanguage.json")
|
||||
return t
|
||||
}
|
||||
# what `path` should read, from what it reads now; null (and a problem) when it cannot be written
|
||||
function sx_regenerate(path: pointer, text: pointer) -> pointer {
|
||||
if s_ends(path, ".tmLanguage.json") { return sx_tm_rewrite(text) }
|
||||
var region: pointer = null
|
||||
if s_ends(path, "ludic_syntax.h") { region = sx_header_region() }
|
||||
if s_ends(path, "ludic-tools/lsp.ludic") { region = sx_lsp_region() }
|
||||
if s_ends(path, "lsp/types.ludic") { region = sx_lsp_types_region() }
|
||||
if s_ends(path, "LudicTokens.kt") { region = sx_kotlin_region() }
|
||||
if s_ends(path, "ludic-mode.el") { region = sx_emacs_region() }
|
||||
let out = sx_splice(text, region)
|
||||
if out == null { push(g_sx_prob, `{path}: no "ludic-dev syntax: begin" ... "end" lines to write between`) }
|
||||
return out
|
||||
}
|
||||
|
||||
# ---- what cannot be generated, checked ----
|
||||
# the words a file's text quotes (every "..." in it), for "does it carry every keyword"
|
||||
function sx_quoted(text: pointer) -> []pointer {
|
||||
let out = new []pointer
|
||||
var i = 0
|
||||
let n = slen(text)
|
||||
while i < n {
|
||||
if text[i] == '"' {
|
||||
var j = i + 1
|
||||
while j < n and text[j] != '"' and text[j] != '\n' { j += 1 }
|
||||
set_add(out, sslice(text, i + 1, j))
|
||||
i = j + 1
|
||||
} else { i += 1 }
|
||||
}
|
||||
return out
|
||||
}
|
||||
# the alternations of a TextMate grammar: every a|b|c word inside \b( )\b
|
||||
function sx_tm_words(text: pointer) -> []pointer {
|
||||
let out = new []pointer
|
||||
let open = bx3('\\', 'b', '(')
|
||||
var i = 0
|
||||
while true {
|
||||
let p = s_index(text, open, i)
|
||||
if p < 0 { break }
|
||||
let e = s_index(text, ")", p) # an alternation holds no ')'; the \\b after it is escaped twice here
|
||||
if e < 0 { break }
|
||||
let alts = split_pipe(sslice(text, p + 3, e))
|
||||
var k = 0
|
||||
while k < len(alts) { set_add(out, alts[k]); k += 1 }
|
||||
i = e + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
# every word of `want` missing from `have` is a problem naming the file
|
||||
function sx_need(file: pointer, what: pointer, want: []pointer, have: []pointer) -> int {
|
||||
var missing = 0
|
||||
var i = 0
|
||||
while i < len(want) {
|
||||
if not set_has(have, want[i]) {
|
||||
push(g_sx_prob, `{file} is missing the {what} {want[i]}`)
|
||||
missing += 1
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
return missing
|
||||
}
|
||||
# does `file` carry every keyword, type and phase of the vocabulary? (the number missing)
|
||||
function sx_carries(file: pointer) -> int {
|
||||
let t = read_file(file)
|
||||
if t == null { push(g_sx_prob, `{file}: cannot read it`); return 1 }
|
||||
var have: []pointer = null
|
||||
if s_ends(file, ".tmLanguage.json") { have = sx_tm_words(t) } else { have = sx_quoted(t) }
|
||||
# the server's other file holds only the words that may stand as names
|
||||
if s_ends(file, "lsp/types.ludic") { return sx_need(file, "keyword that may be a name", sx_contextual(), have) }
|
||||
var n = sx_need(file, "keyword", sx_all_kw(), have)
|
||||
n += sx_need(file, "type", sx_words("types", "name", ""), have)
|
||||
n += sx_need(file, "phase", sx_words("phases", "name", ""), have)
|
||||
return n
|
||||
}
|
||||
|
||||
# docs/language: every keyword, type, phase and attribute is some page's token
|
||||
function sx_docs() -> int {
|
||||
let tokens = new []pointer
|
||||
let pairs = new []pointer
|
||||
collect_docs(tokens, pairs)
|
||||
var n = sx_need("docs/language", "page for the keyword", set_union(sx_all_kw(), sx_kw("constant")), tokens)
|
||||
n += sx_need("docs/language", "page for the type", sx_words("types", "name", ""), tokens)
|
||||
n += sx_need("docs/language", "page for the phase", sx_words("phases", "name", ""), tokens)
|
||||
n += sx_need("docs/language", "page for the attribute", sx_attr_names(), tokens)
|
||||
return n
|
||||
}
|
||||
|
||||
# the parser, scanned: the lower-case words it tests (is_id("w"), text == "w") and the attributes
|
||||
# it reads (a == "Name", ann == "Name") across selfhost/frontend
|
||||
function sx_parser_scan(words: []pointer, attrs: []pointer) -> void {
|
||||
let list = capture("ls selfhost/frontend/*.ludic")
|
||||
var i = 0
|
||||
let n = slen(list)
|
||||
while i < n {
|
||||
let path = s_trim(line_at(list, i))
|
||||
i = i + slen(line_at(list, i)) + 1
|
||||
if slen(path) == 0 or s_ends(path, "/vocab.ludic") { continue }
|
||||
let t = read_file(path)
|
||||
if t == null { continue }
|
||||
add_lower(words, collect_after(t, "is_id(" + dq()))
|
||||
add_lower(words, collect_after(t, "is_kw(" + dq()))
|
||||
add_lower(words, collect_after(t, "text == " + dq()))
|
||||
sx_add_attrs(attrs, collect_after(t, " a == " + dq()))
|
||||
sx_add_attrs(attrs, collect_after(t, "(a == " + dq()))
|
||||
sx_add_attrs(attrs, collect_after(t, " ann == " + dq()))
|
||||
sx_add_attrs(attrs, collect_after(t, "(ann == " + dq()))
|
||||
}
|
||||
}
|
||||
function sx_add_attrs(out: []pointer, src: []pointer) -> void {
|
||||
var i = 0
|
||||
while i < len(src) {
|
||||
let w = src[i]
|
||||
if slen(w) > 0 and sx_ident(w) and not (w == "gltf") { set_add(out, w) } # "gltf": @Asset's argument
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
function sx_ident(w: pointer) -> bool {
|
||||
var i = 0
|
||||
while w[i] != 0 {
|
||||
let c = w[i]
|
||||
if not ((c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') or c == '_') { return false }
|
||||
i += 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
# both directions: nothing the parser tests is missing from the vocabulary, and nothing the
|
||||
# vocabulary lists as read is something the parser never tests
|
||||
function sx_parser() -> int {
|
||||
let words = new []pointer
|
||||
let attrs = new []pointer
|
||||
sx_parser_scan(words, attrs)
|
||||
if len(words) == 0 { push(g_sx_prob, "could not read any word the parser tests from selfhost/frontend"); return 1 }
|
||||
# words the parser tests that are not the language's words: a builtin it notes, a type after `numbers`
|
||||
let known = set_union(set_union(sx_all_kw(), sx_kw("constant")), sx_words("types", "name", ""))
|
||||
let vattrs = sx_words("attributes", "name", "")
|
||||
var n = 0
|
||||
var i = 0
|
||||
while i < len(words) {
|
||||
let w = words[i]
|
||||
if not set_has(known, w) and not set_has(vattrs, w) and not (w == "text_of") {
|
||||
push(g_sx_prob, `the parser tests the word {w}, which vocab.ludic lacks - add its row and run ludic-dev syntax`)
|
||||
n += 1
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
i = 0
|
||||
while i < len(attrs) {
|
||||
if not set_has(vattrs, attrs[i]) {
|
||||
push(g_sx_prob, `the parser reads @{attrs[i]}, which vocab.ludic lacks - add its row and run ludic-dev syntax`)
|
||||
n += 1
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
let kws = set_union(sx_all_kw(), sx_kw("constant"))
|
||||
i = 0
|
||||
while i < len(kws) {
|
||||
if not set_has(words, kws[i]) { push(g_sx_prob, `vocab.ludic lists the keyword {kws[i]}, which the parser never tests`); n += 1 }
|
||||
i += 1
|
||||
}
|
||||
let checked = sx_words("attributes", "name", "checked")
|
||||
i = 0
|
||||
while i < len(checked) {
|
||||
if not set_has(attrs, checked[i]) { push(g_sx_prob, `vocab.ludic lists @{checked[i]} as read, and the parser never reads it`); n += 1 }
|
||||
i += 1
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
# ---- the command ----
|
||||
function sx_report(what: pointer) -> int {
|
||||
if len(g_sx_prob) == 0 { return 0 }
|
||||
err(`{what}:\n`)
|
||||
var i = 0
|
||||
while i < len(g_sx_prob) { err(` - {g_sx_prob[i]}\n`); i += 1 }
|
||||
return 1
|
||||
}
|
||||
# usage: ludic-dev syntax [--check]
|
||||
function cmd_syntax_gen() -> int {
|
||||
let check = argn(2, "") == "--check"
|
||||
if sx_vocab() == null { err("ludic-dev syntax: bin/ludicc has no --emit-syntax (run ludic-dev build)\n"); return 2 }
|
||||
g_sx_prob = new []pointer
|
||||
let ts = sx_targets()
|
||||
var changed = 0
|
||||
var i = 0
|
||||
while i < len(ts) {
|
||||
let path = ts[i]
|
||||
i += 1
|
||||
let text = read_file(path)
|
||||
if text == null { push(g_sx_prob, `{path}: cannot read it`); continue }
|
||||
let want = sx_regenerate(path, text)
|
||||
if want == null or want == text { continue }
|
||||
if check { push(g_sx_prob, `{path} is behind the vocabulary - run ludic-dev syntax`) }
|
||||
else {
|
||||
write_file(path, want)
|
||||
print(` wrote {path}`)
|
||||
changed += 1
|
||||
}
|
||||
}
|
||||
if check {
|
||||
i = 0
|
||||
while i < len(ts) { sx_carries(ts[i]); i += 1 }
|
||||
sx_docs()
|
||||
sx_parser()
|
||||
return sx_report("the vocabulary and what is written from it disagree")
|
||||
}
|
||||
if changed == 0 { print(" every grammar already says what ludicc --emit-syntax does") }
|
||||
return sx_report("ludic-dev syntax")
|
||||
}
|
||||
|
||||
# ---- the regression suite's cases (test.ludic): one line each ----
|
||||
function syntax_cases() -> void {
|
||||
print("== one vocabulary: ludic syntax --json, and every grammar, the server and the docs against it ==")
|
||||
if not shq("bin/ludic syntax --json > /dev/null 2>&1") or sx_vocab() == null { bad("ludic syntax --json printed no vocabulary"); return }
|
||||
# the words editor tooling had lost track of (the scripting proposal's R8)
|
||||
let r8 = ["module", "uses", "port", "bind", "action", "reducer", "dispatch", "registry", "def", "open", "component", "prop", "view", "alias", "friend", "unsafe", "numbers", "of", "as", "from", "attach", "detach"]
|
||||
let kws = sx_all_kw()
|
||||
var gone = ""
|
||||
var i = 0
|
||||
while i < len(r8) { if not set_has(kws, r8[i]) { gone = gone + " " + r8[i] }; i += 1 }
|
||||
if slen(gone) == 0 { ok("ludic syntax --json: every keyword, among them module, uses, port, bind, action, reducer, dispatch, registry, def, component, view") }
|
||||
else { bad2("ludic syntax --json lacks keywords", gone) }
|
||||
let ats = sx_words("attributes", "name", "")
|
||||
let r8a = ["Ref", "OneOf", "Range", "Unit", "Asset", "Color", "Node", "Clip", "Material", "Tint", "Derived", "Text", "Multiline", "Key", "AppendOnly", "ByKey", "PerMap", "Chunked", "max", "owns", "frame", "Sync", "Computed", "alloc_ok"]
|
||||
gone = ""
|
||||
i = 0
|
||||
while i < len(r8a) { if not set_has(ats, r8a[i]) { gone = gone + " @" + r8a[i] }; i += 1 }
|
||||
if slen(gone) == 0 { ok("ludic syntax --json: every attribute, with where it goes, its arguments and its doc") }
|
||||
else { bad2("ludic syntax --json lacks attributes", gone) }
|
||||
let ts = sx_targets()
|
||||
i = 0
|
||||
while i < len(ts) {
|
||||
g_sx_prob = new []pointer
|
||||
let path = ts[i]
|
||||
i += 1
|
||||
let text = read_file(path)
|
||||
var behind = false
|
||||
if text != null {
|
||||
let want = sx_regenerate(path, text)
|
||||
behind = want == null or not (want == text)
|
||||
}
|
||||
let missing = sx_carries(path)
|
||||
if missing == 0 and not behind { ok(`{path} carries the vocabulary's words (as ludic-dev syntax writes them)`) }
|
||||
else if missing > 0 { bad2(`{path} is missing words`, g_sx_prob[0]) }
|
||||
else { bad2(`{path} is behind the vocabulary`, "run ludic-dev syntax") }
|
||||
}
|
||||
g_sx_prob = new []pointer
|
||||
if sx_docs() == 0 { ok("docs/language has a page for every keyword, type, phase and attribute") }
|
||||
else { bad2(`docs/language lacks {string(len(g_sx_prob))} page(s)`, g_sx_prob[0]) }
|
||||
g_sx_prob = new []pointer
|
||||
if sx_parser() == 0 { ok("the parser tests no word and reads no attribute the vocabulary lacks, and the reverse") }
|
||||
else { bad2("the parser and vocab.ludic disagree", g_sx_prob[0]) }
|
||||
}
|
||||
|
|
@ -99,6 +99,30 @@ function diag_json_case(path: pointer, n: int, want: pointer, label: pointer) ->
|
|||
if s_contains(capture(`cat {js}`), want) { ok(label) } else { bad2(label, `no diagnostic holds [{want}]`) }
|
||||
}
|
||||
|
||||
# --stdin-file <path>: stdin stands for <path> wherever the program opens it. A buffer that breaks an
|
||||
# import reached through its barrel (spelt with `..`) is refused at the buffer's line under the file's
|
||||
# usual name; a buffer that fixes a refused entry checks clean; a path nothing opens is a warning.
|
||||
function stdin_file_cases() -> void {
|
||||
let lbl = "--stdin-file: an unsaved buffer checked in place of its file"
|
||||
let fish = "examples/modules/kinds/fishing/index.ludic"
|
||||
let js = `{tmp_dir()}/sf_break.json`
|
||||
let spelt = "./examples/modules/kinds/fishing/../fishing/index.ludic"
|
||||
if shq(`sed 's/return fishing_st.landed/return "seven"/' {fish} | bin/ludicc examples/modules/private_kinds.ludic --check --diagnostics=json --stdin-file {spelt} > {js} 2>/dev/null`) {
|
||||
bad2(lbl, "a buffer with an error checked clean"); return
|
||||
}
|
||||
let broke = capture(`cat {js}`)
|
||||
if not s_contains(broke, `"file": "{fish}", "line": 21`) { bad2(lbl, `no error at the buffer's line 21 of {fish}: {s_trim(broke)}`); return }
|
||||
let js2 = `{tmp_dir()}/sf_fix.json`
|
||||
let fix = "sed -e 's/print(on + 1)/print(1)/' -e 's/half(1, 2)/half(1)/' -e 's/-> int { return \"seven\" }/-> int { return 7 }/'"
|
||||
let clean = shq(`{fix} examples/rejected/every_error.ludic | bin/ludicc examples/rejected/every_error.ludic --check --diagnostics=json --stdin-file examples/rejected/every_error.ludic > {js2} 2>/dev/null`)
|
||||
let said = s_trim(capture(`cat {js2}`))
|
||||
if not clean or not (said == "[]") { bad2(lbl, `a buffer that fixes the saved file's errors said {said}`); return }
|
||||
let js3 = `{tmp_dir()}/sf_none.json`
|
||||
shq(`printf '' | bin/ludicc examples/modules/private_kinds.ludic --check --diagnostics=json --stdin-file examples/nowhere.ludic > {js3} 2>/dev/null`)
|
||||
if not s_contains(capture(`cat {js3}`), "the program does not reach this file") { bad2(lbl, "a path the program never opens was not reported"); return }
|
||||
ok(lbl)
|
||||
}
|
||||
|
||||
# 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
|
||||
|
|
@ -1227,6 +1251,7 @@ function cmd_dev_test() -> int {
|
|||
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", 1, "@Ref(Shops): this program has no Shops - left unresolved", "--diagnostics=json: an @Ref to a registry the program lacks is a warning beside the error")
|
||||
diag_json_case("rejected/node_bad", 8, "crate.shape is \\\"cube\\\", and field Tool.shape is @OneOf(", "--diagnostics=json: every @Node / @Clip / @Material with no model field, @OneOf of the wrong kind or a row outside its words, and the targets the program lacks as warnings")
|
||||
stdin_file_cases()
|
||||
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")
|
||||
|
|
@ -1485,6 +1510,8 @@ function cmd_dev_test() -> int {
|
|||
smoke("library/cursor_capture") # #89 Input.cursor_mode compiles (no-op headless; windowed links cocoa.ll)
|
||||
smoke("rendering/gl_triangle") # Gl.* (OpenGL 4.1 core) compiles headless; the run needs a GPU context
|
||||
|
||||
syntax_cases()
|
||||
|
||||
print("== the compiler and the CLI (ludicc / ludic) ==")
|
||||
# ludicc comes out of the IR seed with clang alone; the CLI is then compiled
|
||||
# by it, from Ludic.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue