Merge lang/foundations (9391695) into lang/editortools: no conflicts, selfhost/ identical to lang/foundations (the seeds unchanged; bootstrap-cfree fixpoint holds at 289532 lines), the help lists R10's --stdin-file, R8's syntax and R9's fmt / remove lines together

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-30 00:26:26 +03:00
commit 50333571d7
76 changed files with 96896 additions and 89729 deletions

View file

@ -25,7 +25,25 @@
"annot-system",
"annot-enginesystem",
"annot-namespace",
"annot-clearcolor"
"annot-clearcolor",
"annot-alloc_ok",
"annot-appendonly",
"annot-asset",
"annot-color",
"annot-derived",
"annot-deterministic",
"annot-frame",
"annot-key",
"annot-max",
"annot-node",
"annot-oneof",
"annot-owns",
"annot-permap",
"annot-range",
"annot-ref",
"annot-text",
"annot-tint",
"annot-unit"
],
"builtins": [
"fn-print",
@ -86,7 +104,8 @@
"kw-machine",
"kw-state",
"kw-become",
"kw-try"
"kw-try",
"kw-dispatch"
],
"ease": [
"ease-in",
@ -119,7 +138,8 @@
"fn-world_register_prop",
"fn-world_attach_dyn",
"fn-world_detach_dyn",
"fn-world_query_next"
"fn-world_query_next",
"kw-system"
],
"events": [
"kw-event",
@ -211,7 +231,8 @@
"op-access",
"op-interp",
"op-literals",
"op-comment"
"op-comment",
"kw-true"
],
"phases": [
"phase-fixedupdate",
@ -240,7 +261,11 @@
"kw-on",
"kw-enter",
"kw-exit",
"kw-start"
"kw-start",
"kw-lasts",
"kw-loads",
"kw-shows",
"kw-then"
],
"screen": [
"screen-clear",
@ -276,7 +301,29 @@
"kw-extern",
"kw-ui",
"kw-enum",
"kw-namespace"
"kw-namespace",
"kw-action",
"kw-alias",
"kw-as",
"kw-bind",
"kw-component",
"kw-def",
"kw-export",
"kw-friend",
"kw-from",
"kw-internal",
"kw-module",
"kw-mut",
"kw-numbers",
"kw-of",
"kw-open",
"kw-port",
"kw-prop",
"kw-reducer",
"kw-registry",
"kw-unsafe",
"kw-uses",
"kw-view"
],
"system": [
"system-run",

View file

@ -146,11 +146,21 @@ output. The formatter cannot change what a program means.
## Keeping it honest
The vocabulary is written down in five places that cannot include each other —
the compiler's two tables, `ludic_syntax.h`, the TextMate grammar (JSON), and the
JetBrains lexer (Kotlin). Adding a builtin and forgetting the rest is silent
failure, so `bin/ludic-dev check-vocabulary` (written in Ludic) compares all five, and
`bin/ludic-dev test-tools` runs it.
The language's words come from one place: the compiler's vocabulary table
(`selfhost/frontend/vocab.ludic`), printed by `ludic syntax --json` (`ludicc
--emit-syntax`) and held to the parser's own recognisers. The keyword, type and
phase lists in `ludic_syntax.h`, the TextMate grammar (every pattern marked
`"comment": "ludic-dev syntax: <group>"`), the JetBrains lexer's
`LudicVocabulary`, `ludic-mode.el` and the language server are written from it
between `ludic-dev syntax: begin` / `end` lines by `bin/ludic-dev syntax`; do not
edit inside them.
When you add a keyword or builtin: put it in `ludic_syntax.h`, then run
`bin/ludic-dev test-tools` and let it tell you which copies still need it.
`bin/ludic-dev syntax --check` (and `check-vocabulary`, which `test-tools` runs,
and the regression suite) fails when a generated list is behind, when a grammar
lacks a word, when docs/language has no page for a keyword, type, phase or
attribute, or when the parser (`selfhost/frontend`) tests a word or reads an
attribute the vocabulary lacks. Builtins are still listed in `ludic_syntax.h`,
and `check-vocabulary` compares them with the grammar and the Kotlin lexer.
When you add a keyword or an attribute: add its row to `vocab.ludic`, rebuild the
compiler, run `bin/ludic-dev syntax`, and write its docs/language page.

View file

@ -10,23 +10,27 @@
;;; Code:
;; The vocabulary mirrors tools/ludic-tools/ludic_syntax.h; that header is the reference.
;; The vocabulary is written by `ludic-dev syntax` from the compiler's own table
;; (`ludicc --emit-syntax`) between the marked lines.
;; ludic-dev syntax: begin - generated from `ludicc --emit-syntax` (selfhost/frontend/vocab.ludic); run `ludic-dev syntax`, do not edit
(defconst ludic--declaration-keywords
'("program" "import" "property" "model" "enum" "ui" "namespace"
"const" "var" "function" "extern" "handler" "entry" "event" "scene" "test" "prefab"))
'("program" "module" "import" "property" "state" "event" "action" "reducer" "enum" "model"
"prefab" "scene" "handler" "ui" "namespace" "alias" "port" "bind" "registry" "def"
"component" "view" "prop" "const" "var" "function" "fn" "extern" "entry" "test"))
(defconst ludic--clause-keywords
'("phase" "query" "on" "cancellable" "public" "layer" "start" "shows" "lasts" "loads" "then" "export" "internal"))
'("export" "internal" "friend" "uses" "open" "of" "as" "from" "numbers" "unsafe" "mut" "phase"
"query" "on" "cancellable" "public" "layer" "start" "shows" "lasts" "then" "loads"
"system"))
(defconst ludic--statement-keywords
'("let" "return" "if" "else" "while" "for" "in" "spawn" "despawn"
"enable" "disable" "attach" "detach" "match" "machine" "state" "become" "where"
"and" "or" "not" "break" "continue" "new" "emit" "cancel" "try"))
'("let" "if" "else" "while" "for" "in" "where" "match" "machine" "become" "return" "break"
"continue" "new" "spawn" "despawn" "enable" "disable" "attach" "detach" "emit" "cancel"
"dispatch" "try" "and" "or" "not"))
(defconst ludic--types
'("int" "long" "fixed" "countdown" "bool" "entity" "string" "pointer" "byte"
"words" "fixeds" "pointers" "Vector" "IVec2" "Rect" "void"))
(defconst ludic--phases '("Start" "Input" "FixedUpdate" "Update" "LateUpdate" "Render" "Overlay"))
'("int" "long" "float" "double" "fixed" "bool" "byte" "string" "Key" "pointer" "void" "entity"
"countdown" "words" "floats" "doubles" "fixeds" "pointers" "Vector" "IVec2" "Rect"))
(defconst ludic--phases
'("Start" "Input" "FixedUpdate" "Update" "LateUpdate" "Render" "Overlay"))
;; ludic-dev syntax: end
(defconst ludic-font-lock-keywords
(list

View file

@ -55,24 +55,37 @@ object LudicTokens {
}
/**
* The vocabulary, mirroring tools/ludic-tools/ludic_syntax.h. When the language
* grows a keyword both sides change together; that header is the reference.
* The vocabulary. The keyword, type and phase sets are written by `ludic-dev
* syntax` from the compiler's own table (`ludicc --emit-syntax`); the rest
* mirrors tools/ludic-tools/ludic_syntax.h.
*/
object LudicVocabulary {
// ludic-dev syntax: begin - generated from `ludicc --emit-syntax` (selfhost/frontend/vocab.ludic); run `ludic-dev syntax`, do not edit
val DECL = setOf(
"program", "import", "property", "model", "enum", "ui", "namespace",
"const", "var", "function", "extern", "handler", "entry", "event", "scene", "test"
"program", "module", "import", "property", "state", "event", "action", "reducer", "enum",
"model", "prefab", "scene", "handler", "ui", "namespace", "alias", "port", "bind",
"registry", "def", "component", "view", "prop", "const", "var", "function", "fn", "extern",
"entry", "test"
)
val CLAUSE = setOf(
"phase", "query", "on", "cancellable", "public", "layer", "start"
"export", "internal", "friend", "uses", "open", "of", "as", "from", "numbers", "unsafe",
"mut", "phase", "query", "on", "cancellable", "public", "layer", "start", "shows", "lasts",
"then", "loads", "system"
)
val STMT = setOf(
"let", "return", "if", "else", "while", "for", "in", "spawn", "despawn",
"enable", "disable", "match", "machine", "prefab", "state", "become", "where",
"and", "or", "not", "break", "continue", "new", "emit", "cancel", "try"
"let", "if", "else", "while", "for", "in", "where", "match", "machine", "become", "return",
"break", "continue", "new", "spawn", "despawn", "enable", "disable", "attach", "detach",
"emit", "cancel", "dispatch", "try", "and", "or", "not"
)
val PRIMITIVES = setOf("int", "long", "fixed", "float", "double", "countdown", "bool", "entity", "string", "pointer", "byte", "words", "fixeds", "floats", "doubles", "pointers", "Vector", "IVec2", "Rect", "void")
val PHASES = setOf("Start", "Input", "FixedUpdate", "Update", "LateUpdate", "Render", "Overlay")
val PRIMITIVES = setOf(
"int", "long", "float", "double", "fixed", "bool", "byte", "string", "Key", "pointer",
"void", "entity", "countdown", "words", "floats", "doubles", "fixeds", "pointers", "Vector",
"IVec2", "Rect"
)
val PHASES = setOf(
"Start", "Input", "FixedUpdate", "Update", "LateUpdate", "Render", "Overlay"
)
// ludic-dev syntax: end
val WIDGETS = setOf("panel", "col", "row", "label", "button", "image", "spacer")
val BUILTINS = setOf(

View file

@ -3,7 +3,7 @@
"name": "Ludic",
"scopeName": "source.ludic",
"fileTypes": ["ludic"],
"comment": "Static grammar for Ludic. It is the fallback: when ludic-lsp is running, its semantic tokens refine everything here with real name resolution. Generated shapes are kept in sync with tools/ludic-tools/ludic_syntax.h.",
"comment": "Static grammar for Ludic. It is the fallback: when ludic-lsp is running, its semantic tokens refine everything here with real name resolution. The keyword, type and phase alternations are written by `ludic-dev syntax` from the compiler's own vocabulary (`ludicc --emit-syntax`): every pattern marked \"ludic-dev syntax: <group>\".",
"patterns": [
{ "include": "#comment" },
{ "include": "#declaration" },
@ -126,7 +126,7 @@
"captures": { "1": { "name": "keyword.control.import.ludic" } }
},
{
"match": "\\b(phase)\\s+(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b",
"comment": "ludic-dev syntax: phase-clause", "match": "\\b(phase)\\s+(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b",
"captures": {
"1": { "name": "keyword.other.clause.ludic" },
"2": { "name": "constant.language.phase.ludic" }
@ -180,14 +180,13 @@
},
"keyword": {
"patterns": [
{ "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|become|return|spawn|despawn|enable|disable|attach|detach|emit|cancel|where|break|continue|new|try)\\b" },
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on|cancellable|public|layer|start|shows|lasts|loads|then|export|internal)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|prefab|namespace|enum|ui|const|var|let|function|fn|handler|entry|event|scene|state|test)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|float|double|countdown|bool|entity|string|pointer|byte|words|fixeds|floats|doubles|pointers|Vector|IVec2|Rect|void)\\b" },
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b" }
{ "name": "keyword.control.ludic", "comment": "ludic-dev syntax: statement", "match": "\\b(let|if|else|while|for|in|where|match|machine|become|return|break|continue|new|spawn|despawn|enable|disable|attach|detach|emit|cancel|dispatch|try)\\b" },
{ "name": "keyword.operator.logical.ludic", "comment": "ludic-dev syntax: operator", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "comment": "ludic-dev syntax: modifier", "match": "\\b(export|internal|friend|uses|open|of|as|from|numbers|unsafe|mut|phase|query|on|cancellable|public|layer|start|shows|lasts|then|loads|system)\\b" },
{ "name": "storage.type.ludic", "comment": "ludic-dev syntax: declaration", "match": "\\b(program|module|import|property|state|event|action|reducer|enum|model|prefab|scene|handler|ui|namespace|alias|port|bind|registry|def|component|view|prop|const|var|function|fn|extern|entry|test)\\b" },
{ "name": "support.type.primitive.ludic", "comment": "ludic-dev syntax: types", "match": "\\b(int|long|float|double|fixed|bool|byte|string|Key|pointer|void|entity|countdown|words|floats|doubles|fixeds|pointers|Vector|IVec2|Rect)\\b" },
{ "name": "constant.language.boolean.ludic", "comment": "ludic-dev syntax: constant", "match": "\\b(true|false|null)\\b" },
{ "name": "constant.language.phase.ludic", "comment": "ludic-dev syntax: phases", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b" }
]
},
"widget": {
@ -241,7 +240,7 @@
"patterns": [
{ "name": "keyword.operator.arrow.ludic", "match": "->|=>" },
{ "name": "keyword.operator.range.ludic", "match": "\\.\\." },
{ "name": "keyword.operator.bitwise.ludic", "match": "<<|>>|&|\\|\\^|~" },
{ "name": "keyword.operator.bitwise.ludic", "match": "<<|>>|&|\\||\\^|~" },
{ "name": "keyword.operator.comparison.ludic", "match": "==|!=|<=|>=|<|>" },
{ "name": "keyword.operator.assignment.ludic", "match": "\\+=|-=|\\*=|/=|=" },
{ "name": "keyword.operator.arithmetic.ludic", "match": "[+\\-*/%]" }

View file

@ -3,7 +3,7 @@
"name": "Ludic",
"scopeName": "source.ludic",
"fileTypes": ["ludic"],
"comment": "Static grammar for Ludic. It is the fallback: when ludic-lsp is running, its semantic tokens refine everything here with real name resolution. Generated shapes are kept in sync with tools/ludic-tools/ludic_syntax.h.",
"comment": "Static grammar for Ludic. It is the fallback: when ludic-lsp is running, its semantic tokens refine everything here with real name resolution. The keyword, type and phase alternations are written by `ludic-dev syntax` from the compiler's own vocabulary (`ludicc --emit-syntax`): every pattern marked \"ludic-dev syntax: <group>\".",
"patterns": [
{ "include": "#comment" },
{ "include": "#declaration" },
@ -126,7 +126,7 @@
"captures": { "1": { "name": "keyword.control.import.ludic" } }
},
{
"match": "\\b(phase)\\s+(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b",
"comment": "ludic-dev syntax: phase-clause", "match": "\\b(phase)\\s+(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b",
"captures": {
"1": { "name": "keyword.other.clause.ludic" },
"2": { "name": "constant.language.phase.ludic" }
@ -180,14 +180,13 @@
},
"keyword": {
"patterns": [
{ "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|become|return|spawn|despawn|enable|disable|attach|detach|emit|cancel|where|break|continue|new|try)\\b" },
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on|cancellable|public|layer|start|shows|lasts|loads|then|export|internal)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|prefab|namespace|enum|ui|const|var|let|function|fn|handler|entry|event|scene|state|test)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|float|double|countdown|bool|entity|string|pointer|byte|words|fixeds|floats|doubles|pointers|Vector|IVec2|Rect|void)\\b" },
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b" }
{ "name": "keyword.control.ludic", "comment": "ludic-dev syntax: statement", "match": "\\b(let|if|else|while|for|in|where|match|machine|become|return|break|continue|new|spawn|despawn|enable|disable|attach|detach|emit|cancel|dispatch|try)\\b" },
{ "name": "keyword.operator.logical.ludic", "comment": "ludic-dev syntax: operator", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "comment": "ludic-dev syntax: modifier", "match": "\\b(export|internal|friend|uses|open|of|as|from|numbers|unsafe|mut|phase|query|on|cancellable|public|layer|start|shows|lasts|then|loads|system)\\b" },
{ "name": "storage.type.ludic", "comment": "ludic-dev syntax: declaration", "match": "\\b(program|module|import|property|state|event|action|reducer|enum|model|prefab|scene|handler|ui|namespace|alias|port|bind|registry|def|component|view|prop|const|var|function|fn|extern|entry|test)\\b" },
{ "name": "support.type.primitive.ludic", "comment": "ludic-dev syntax: types", "match": "\\b(int|long|float|double|fixed|bool|byte|string|Key|pointer|void|entity|countdown|words|floats|doubles|fixeds|pointers|Vector|IVec2|Rect)\\b" },
{ "name": "constant.language.boolean.ludic", "comment": "ludic-dev syntax: constant", "match": "\\b(true|false|null)\\b" },
{ "name": "constant.language.phase.ludic", "comment": "ludic-dev syntax: phases", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b" }
]
},
"widget": {
@ -241,7 +240,7 @@
"patterns": [
{ "name": "keyword.operator.arrow.ludic", "match": "->|=>" },
{ "name": "keyword.operator.range.ludic", "match": "\\.\\." },
{ "name": "keyword.operator.bitwise.ludic", "match": "<<|>>|&|\\|\\^|~" },
{ "name": "keyword.operator.bitwise.ludic", "match": "<<|>>|&|\\||\\^|~" },
{ "name": "keyword.operator.comparison.ludic", "match": "==|!=|<=|>=|<|>" },
{ "name": "keyword.operator.assignment.ludic", "match": "\\+=|-=|\\*=|/=|=" },
{ "name": "keyword.operator.arithmetic.ludic", "match": "[+\\-*/%]" }

View file

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

View file

@ -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"
@ -68,6 +69,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")
@ -109,6 +111,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() }

View file

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

View file

@ -34,6 +34,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"
@ -56,6 +57,7 @@ program Ludic {
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("")
@ -116,6 +118,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() }

View file

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

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

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

View file

@ -1518,6 +1518,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.

View file

@ -48,6 +48,11 @@ program LudicLsp {
const LS_QUERYVAR: int = 15
const LS_STATE: int = 16
const LS_IMPORT: int = 17
const LS_REGISTRY: int = 18 # registry R of T: a table of named entries
const LS_ENUM: int = 19
const LS_ENUMMEMBER: int = 20
const LS_VIEW: int = 21 # a UI component or a view
const LS_REDUCER: int = 22 # reducer S on A: named "S on A" in the outline
# ---- semantic classes ----
const SC_NONE: int = 0
@ -175,6 +180,7 @@ program LudicLsp {
exported: int = 0
init: int = 0 # first token of the initializer (let/var/const), or -1
mutable: int = 0 # a `var` local
decl: pointer = null # the word that declared it (property, state, event, action, port, ...)
}
property Doc {
path: pointer = null
@ -226,21 +232,38 @@ program LudicLsp {
if c == ',' or c == ':' or c == '.' or c == '!' or c == '@' or c == ';' { return true } # , : . ! @ ;
return c == '&' or c == '|' or c == '^' or c == '~' # & | ^ ~
}
# the vocabulary, written by `ludic-dev syntax` from `ludicc --emit-syntax`
# ludic-dev syntax: begin - generated from `ludicc --emit-syntax` (selfhost/frontend/vocab.ludic); run `ludic-dev syntax`, do not edit
function is_type_word(w: pointer) -> bool {
return (w == "int") or (w == "long") or (w == "fixed") or (w == "float") or (w == "double") or (w == "floats") or (w == "doubles") or (w == "countdown") or (w == "bool") or (w == "entity") or (w == "string") or (w == "pointer") or (w == "byte") or (w == "words") or (w == "fixeds") or (w == "pointers") or (w == "Vector") or (w == "IVec2") or (w == "Rect") or (w == "void")
}
function is_phase_word(w: pointer) -> bool {
return (w == "Start") or (w == "Input") or (w == "FixedUpdate") or (w == "Update") or (w == "LateUpdate") or (w == "Render")
}
function is_keyword_word(w: pointer) -> bool {
if (w == "program") or (w == "import") or (w == "property") or (w == "model") or (w == "enum") or (w == "ui") { return true }
if (w == "const") or (w == "var") or (w == "function") or (w == "extern") or (w == "handler") or (w == "entry") or (w == "event") or (w == "scene") or (w == "mut") { return true }
if (w == "phase") or (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true }
if (w == "let") or (w == "return") or (w == "if") or (w == "else") or (w == "while") or (w == "for") or (w == "in") or (w == "spawn") or (w == "despawn") { return true }
if (w == "enable") or (w == "disable") or (w == "match") or (w == "machine") or (w == "state") or (w == "become") or (w == "where") or (w == "prefab") { return true }
if (w == "and") or (w == "or") or (w == "not") or (w == "break") or (w == "continue") or (w == "new") or (w == "emit") or (w == "cancel") { return true }
if (w == "int") or (w == "long") or (w == "float") or (w == "double") or (w == "fixed") or (w == "bool") { return true }
if (w == "byte") or (w == "string") or (w == "Key") or (w == "pointer") or (w == "void") or (w == "entity") { return true }
if (w == "countdown") or (w == "words") or (w == "floats") or (w == "doubles") or (w == "fixeds") or (w == "pointers") { return true }
if (w == "Vector") or (w == "IVec2") or (w == "Rect") { return true }
return false
}
function is_phase_word(w: pointer) -> bool {
if (w == "Start") or (w == "Input") or (w == "FixedUpdate") or (w == "Update") or (w == "LateUpdate") or (w == "Render") { return true }
if (w == "Overlay") { return true }
return false
}
function is_keyword_word(w: pointer) -> bool {
if (w == "program") or (w == "module") or (w == "import") or (w == "property") or (w == "state") or (w == "event") { return true }
if (w == "action") or (w == "reducer") or (w == "enum") or (w == "model") or (w == "prefab") or (w == "scene") { return true }
if (w == "handler") or (w == "ui") or (w == "namespace") or (w == "alias") or (w == "port") or (w == "bind") { return true }
if (w == "registry") or (w == "def") or (w == "component") or (w == "view") or (w == "prop") or (w == "const") { return true }
if (w == "var") or (w == "function") or (w == "fn") or (w == "extern") or (w == "entry") or (w == "test") { return true }
if (w == "export") or (w == "internal") or (w == "friend") or (w == "uses") or (w == "open") or (w == "of") { return true }
if (w == "as") or (w == "from") or (w == "numbers") or (w == "unsafe") or (w == "mut") or (w == "phase") { return true }
if (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true }
if (w == "shows") or (w == "lasts") or (w == "then") or (w == "loads") or (w == "system") or (w == "let") { return true }
if (w == "if") or (w == "else") or (w == "while") or (w == "for") or (w == "in") or (w == "where") { return true }
if (w == "match") or (w == "machine") or (w == "become") or (w == "return") or (w == "break") or (w == "continue") { return true }
if (w == "new") or (w == "spawn") or (w == "despawn") or (w == "enable") or (w == "disable") or (w == "attach") { return true }
if (w == "detach") or (w == "emit") or (w == "cancel") or (w == "dispatch") or (w == "try") or (w == "and") { return true }
if (w == "or") or (w == "not") { return true }
return false
}
# ludic-dev syntax: end
function is_widget_word(w: pointer) -> bool {
return (w == "panel") or (w == "col") or (w == "row") or (w == "label") or (w == "button") or (w == "image") or (w == "spacer")
}
@ -449,7 +472,7 @@ program LudicLsp {
function sym_add(kind: int, nametok: int, parent: int) -> int {
let s = new Sym
s.kind = kind; s.tok = nametok; s.parent = parent
s.name = ""; s.ty = ""; s.detail = ""; s.doc = ""; s.init = -1; s.mutable = 0
s.name = ""; s.ty = ""; s.detail = ""; s.doc = ""; s.init = -1; s.mutable = 0; s.decl = ""
if nametok >= 0 { s.start = pD.tk_start[nametok]; s.end = pD.tk_end[nametok]; s.name = ttext(pD, nametok) }
s.bstart = 0; s.bend = cstr_len(pD.text)
push(pD.syms, s)
@ -457,6 +480,62 @@ program LudicLsp {
}
function cur_match() -> int { if pi < ntok(pD) { return pD.tmatch[pi] }; return -1 }
function skip_group() -> void { let m = cur_match(); if m < 0 { padv(); return }; pi = m + 1; pskipnl() }
# past a bracketed group, staying on its line (skip_group also takes the newlines after it)
function lp_jump_group() -> void { let m = cur_match(); if m < 0 { pi += 1 } else { pi = m + 1 } }
# the tokens up to the end of this line as written, joined by single spaces (after a leading one)
function lp_rest_of_line() -> pointer {
let b = buf_new()
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL {
if pk() != LT_COMMENT { buf_putc(b, ' '); buf_puts(b, ttext(pD, pi)) }
pi += 1
}
return buf_str(b)
}
# a component's or a view's body: its props and state (and a view's fields) as fields, its
# functions and events as functions
function lp_view_body(sidx: int) -> void {
let close = cur_match()
pD.syms[sidx].bstart = pD.tk_start[pi]
if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) }
padv()
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
if close >= 0 and pi >= close { break }
let before = pi
if pis("function") { lp_decl(sidx) }
else if pis("on") {
padv()
let nt = pname()
if nt >= 0 {
let f = sym_add(LS_FN, nt, sidx)
pD.syms[f].detail = "on " + pD.syms[f].name
if pis("(") { lp_params(f, LS_PARAM, buf_new()) }
}
while pi < ntok(pD) and pk() != LT_EOF and not pis("{") and pk() != LT_NL { pi += 1 }
if pis("{") { skip_group() }
}
else {
var word = ""
if pis("prop") or pis("state") { word = pword(); padv() }
let ft = pname()
if ft >= 0 and (pis(":") or pis("=")) {
let f = sym_add(LS_FIELD, ft, sidx)
if pis(":") { padv(); pD.syms[f].ty = lp_type_text() }
var head = word
if (head == "") { head = pD.syms[sidx].name + "." }
else { head = head + " " }
pD.syms[f].detail = head + pD.syms[f].name + ": " + pD.syms[f].ty
}
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis(",") and not pis("}") {
if close >= 0 and pi >= close { break }
if pis("(") or pis("{") or pis("[") { lp_jump_group() } else { pi += 1 }
}
}
if pi == before { padv() } else { pskipnl() }
if pis(",") { padv() }
}
if close >= 0 { pi = close + 1 } else { padv() }
pskipnl()
}
# `name: Type` pairs inside ( ) — record params, build the signature into sig
function lp_params(owner: int, kind: int, sig: Buf) -> void {
@ -498,7 +577,28 @@ program LudicLsp {
}
let tt = pname()
if tt < 0 { return "" }
return pre + ttext(pD, tt)
var ty = pre + ttext(pD, tt)
# a function type, `fn(int, float) -> bool`, and a generic's arguments, `Pool<Item>`, as written
if (ttext(pD, tt) == "fn") and pis("(") {
let m = cur_match()
if m >= 0 {
ty = ty + pD.src[pD.tk_start[pi]..pD.tk_end[m]]
pi = m + 1
if pis("->") { pi += 1; ty = ty + " -> " + lp_type_text() }
}
}
else if pis("<") {
let a = pi
var depth = 0
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL {
if pis("<") { depth += 1 }
if pis(">") { depth -= 1 }
pi += 1
if depth == 0 { break }
}
ty = ty + pD.src[pD.tk_start[a]..pD.tk_end[pi - 1]]
}
return ty
}
# (a, b) variable list -> token indices into out; returns count
@ -641,10 +741,33 @@ program LudicLsp {
}
}
var lp_anchor: int = -1 # where a declaration begins when `export`, `unsafe` or @attributes lead it
# one top-level declaration
function lp_decl(parent: int) -> void {
let w = pword()
let decl_tok = pi
var decl_tok = pi
if lp_anchor >= 0 { decl_tok = lp_anchor; lp_anchor = -1 }
# a lead-in: @Attr(args) (a registry's @AppendOnly, a handler's @On(E)), `export` or `unsafe`
# before a declaration - read past, the declaration keeps where it began and says it is exported
if pk() == LT_ANNO {
padv()
if pis("(") { skip_group() }
lp_anchor = decl_tok
lp_decl(parent)
return
}
if (w == "export" or w == "unsafe") and pi + 1 < ntok(pD) and not tis(pD, pi + 1, "function") and not tis(pD, pi + 1, "pure") {
let first = len(pD.syms)
padv()
lp_anchor = decl_tok
lp_decl(parent)
if w == "export" {
var q = first
while q < len(pD.syms) { if pD.syms[q].parent == parent { pD.syms[q].exported = 1 }; q += 1 }
}
return
}
if w == "import" {
padv()
@ -660,18 +783,23 @@ program LudicLsp {
}
return
}
var wd = w
if w == "export" and tis(pD, pi + 1, "state") { padv(); wd = "state" }
if (wd == "property") or (wd == "model") or (wd == "state") { # a state: a record with one instance
let is_comp = (wd == "property") or (wd == "state")
padv(); let nt = pname(); if nt < 0 { return }
let wd = w
# records: a property, a state (one instance), an event, an action, a port (its members are
# fn-typed), a model (its members are components)
if (wd == "property") or (wd == "model") or (wd == "state") or (wd == "event") or (wd == "action") or (wd == "port") {
let is_comp = not (wd == "model")
padv()
if wd == "event" and pis("cancellable") { padv() }
let nt = pname(); if nt < 0 { return }
var kind = LS_ARCHETYPE
if is_comp { kind = LS_COMPONENT }
let sidx = sym_add(kind, nt, parent)
pD.syms[sidx].decl = wd
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
while pk() == LT_ANNO { padv() }
if pis("<") { while pi < ntok(pD) and not pis(">") and pk() != LT_EOF { padv() }; padv() } # property Pool<T>
while pk() == LT_ANNO { padv(); if pis("(") { skip_group() } }
let det = buf_new()
buf_puts(det, w); buf_putc(det, ' '); buf_puts(det, pD.syms[sidx].name); buf_puts(det, " {")
buf_puts(det, wd); buf_putc(det, ' '); buf_puts(det, pD.syms[sidx].name); buf_puts(det, " {")
if not pis("{") { pD.syms[sidx].detail = buf_str(det); return }
let close = cur_match()
pD.syms[sidx].bstart = pD.tk_start[pi]
@ -681,14 +809,19 @@ program LudicLsp {
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
if close >= 0 and pi >= close { break }
if pis(",") { padv(); continue }
while pk() == LT_ANNO { padv() } # @Sync / @Computed on a field
while pk() == LT_ANNO { padv(); if pis("(") { skip_group() } } # @Ref(Items), @Range(0, 1), @Sync ...
let ft = pname(); if ft < 0 { padv(); continue }
let f = sym_add(LS_FIELD, ft, sidx)
var fty = ""
if pis(":") { padv(); fty = lp_type_text() }
pD.syms[f].ty = fty
pD.syms[f].detail = concat3(pD.syms[sidx].name, ".", concat3(pD.syms[f].name, ": ", fty))
if pis("=") { padv(); while pi < ntok(pD) and not pis(",") and not pis("}") and pk() != LT_EOF { if pis("(") or pis("{") or pis("[") { skip_group() } else { padv() } } }
# the rest of the member - a fn type's parameters and result, a generic's arguments, the
# default - ends at a comma, the closing brace or the end of its line
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis(",") and not pis("}") {
if close >= 0 and pi >= close { break }
if pis("(") or pis("{") or pis("[") { lp_jump_group() } else { pi += 1 }
}
if not first { buf_putc(det, ',') }
buf_putc(det, ' '); buf_puts(det, pD.syms[f].name); buf_puts(det, ": "); buf_puts(det, fty)
first = false
@ -699,9 +832,86 @@ program LudicLsp {
pskipnl()
return
}
if (w == "const") or (w == "var") {
if w == "open" and tis(pD, pi + 1, "registry") { padv(); lp_anchor = decl_tok; lp_decl(parent); return }
if w == "registry" { # registry Name of Record [as P] [from "file.lres"]
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(LS_REGISTRY, nt, parent)
pD.syms[sidx].decl = "registry"
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
if pis("of") { pD.syms[sidx].ty = ttext(pD, pi + 1) } # its record, for go-to-type
pD.syms[sidx].detail = "registry " + pD.syms[sidx].name + lp_rest_of_line()
pskipnl()
return
}
if w == "enum" { # enum Name { A, B = 2, C }
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(LS_ENUM, nt, parent)
pD.syms[sidx].decl = "enum"
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
pD.syms[sidx].detail = "enum " + pD.syms[sidx].name
if pis("{") {
let close = cur_match()
pD.syms[sidx].bstart = pD.tk_start[pi]
if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] }
padv()
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
if close >= 0 and pi >= close { break }
let mt = pname()
if mt < 0 { padv(); continue }
let m = sym_add(LS_ENUMMEMBER, mt, sidx)
pD.syms[m].detail = pD.syms[sidx].name + "." + pD.syms[m].name
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis(",") and not pis("}") { pi += 1 }
if pis(",") { padv() } else { pskipnl() }
}
if close >= 0 { pi = close + 1 } else { padv() }
pskipnl()
}
return
}
if (w == "component") or (w == "view") { # component Name[(states)] { prop / state / function / on }
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(LS_VIEW, nt, parent)
pD.syms[sidx].decl = w
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
pD.syms[sidx].detail = w + " " + pD.syms[sidx].name
if pis("(") { lp_params(sidx, LS_PARAM, buf_new()) }
if pis("{") { lp_view_body(sidx) }
return
}
if w == "reducer" { # reducer State on Action(states, a: Action) { ... }
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(LS_REDUCER, nt, parent)
pD.syms[sidx].decl = "reducer"
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
if pis("on") { padv(); let at = pname(); if at >= 0 { pD.syms[sidx].name = pD.syms[sidx].name + " on " + ttext(pD, at) } }
pD.syms[sidx].detail = "reducer " + pD.syms[sidx].name
if pis("(") { lp_params(sidx, LS_PARAM, buf_new()) }
if pis("{") {
let close = cur_match()
pD.syms[sidx].bstart = pD.tk_start[pi]
if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) }
var q = 0
while q < len(pD.syms) { if pD.syms[q].parent == sidx and pD.syms[q].kind == LS_PARAM { pD.syms[q].bstart = pD.syms[sidx].bstart; pD.syms[q].bend = pD.syms[sidx].bend }; q += 1 }
lp_block(sidx, pD.syms[sidx].bend)
}
return
}
# a def's entry and a bind's answers are data: their bodies hold no declaration
if (w == "def") or (w == "bind") {
padv()
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis("{") { pi += 1 }
if pis("{") { skip_group() } else { pskipnl() }
return
}
# a line that says what a file is: module / friend module / numbers float
if (w == "module") or (w == "friend") or (w == "numbers") {
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL { pi += 1 }
pskipnl()
return
}
if (w == "const") or (w == "var") or (w == "let") {
var kind = LS_VAR
if w == "const" { kind = LS_CONST }
if (w == "const") or (w == "let") { kind = LS_CONST }
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(kind, nt, parent)
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
@ -922,10 +1132,10 @@ program LudicLsp {
# ============================================================================
function is_local_kind(k: int) -> bool { return k == LS_LOCAL or k == LS_PARAM or k == LS_QUERYVAR or k == LS_STATE }
function is_toplevel_kind(k: int) -> bool {
return k == LS_COMPONENT or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER or k == LS_UNIT
return k == LS_COMPONENT or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER or k == LS_UNIT or k == LS_REGISTRY or k == LS_ENUM or k == LS_VIEW
}
function is_outline_kind(k: int) -> bool {
return k == LS_UNIT or k == LS_COMPONENT or k == LS_FIELD or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER
return k == LS_UNIT or k == LS_COMPONENT or k == LS_FIELD or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER or k == LS_REGISTRY or k == LS_ENUM or k == LS_ENUMMEMBER or k == LS_VIEW or k == LS_REDUCER
}
# ---- paths & URIs ----
@ -1199,6 +1409,37 @@ program LudicLsp {
}
return best
}
# a name the whole unit sees: a top-level kind, and not a member of a component or a view
function is_top_sym(D: Doc, k: int) -> bool {
let y = D.syms[k]
if not is_toplevel_kind(y.kind) { return false }
return y.parent < 0 or D.syms[y.parent].kind != LS_VIEW
}
# the field an attribute's argument names - `@Node(model)`, `@Clip(model)` - in the record the
# attribute is written in, or -1 (a registry, `@Ref(Items)`, and a constant resolve as any name)
function attr_arg_field(D: Doc, tok: int) -> int {
var p = prev_sig(D, tok)
while p >= 0 and (tis(D, p, ",") or D.tk_kind[p] == LT_ID or D.tk_kind[p] == LT_STR or D.tk_kind[p] == LT_INT or D.tk_kind[p] == LT_FLOAT) { p = prev_sig(D, p) }
if p < 0 or not tis(D, p, "(") { return -1 }
let a = prev_sig(D, p)
if a < 0 or D.tk_kind[a] != LT_ANNO { return -1 }
let off = D.tk_start[tok]
let name = ttext(D, tok)
var rec = -1
var i = 0
while i < len(D.syms) {
let y = D.syms[i]
if (y.kind == LS_COMPONENT or y.kind == LS_ARCHETYPE) and off >= y.bstart and off <= y.bend and y.bend > y.bstart { rec = i }
i += 1
}
if rec < 0 { return -1 }
i = 0
while i < len(D.syms) {
if D.syms[i].kind == LS_FIELD and D.syms[i].parent == rec and (D.syms[i].name == name) { return i }
i += 1
}
return -1
}
# returns sym index in g_owner (set), or -1
function find_top(D: Doc, name: pointer) -> int {
let rel = related(D)
@ -1206,7 +1447,7 @@ program LudicLsp {
while i < len(rel) {
var k = 0
while k < len(rel[i].syms) {
if is_toplevel_kind(rel[i].syms[k].kind) and (rel[i].syms[k].name == name) { g_owner = rel[i]; return k }
if is_top_sym(rel[i], k) and (rel[i].syms[k].name == name) { g_owner = rel[i]; return k }
k += 1
}
i += 1
@ -1215,7 +1456,7 @@ program LudicLsp {
while i < len(g_docs) {
var k = 0
while k < len(g_docs[i].syms) {
if is_toplevel_kind(g_docs[i].syms[k].kind) and (g_docs[i].syms[k].name == name) { g_owner = g_docs[i]; return k }
if is_top_sym(g_docs[i], k) and (g_docs[i].syms[k].name == name) { g_owner = g_docs[i]; return k }
k += 1
}
i += 1
@ -1259,6 +1500,8 @@ program LudicLsp {
}
let l = find_local(D, name, D.tk_start[tok])
if l >= 0 { g_owner = D; g_symidx = l; return tok }
let af = attr_arg_field(D, tok)
if af >= 0 { g_owner = D; g_symidx = af; return tok }
let t = find_top(D, name)
if t >= 0 { g_symidx = t; return tok }
g_owner = null; g_symidx = -1
@ -1664,6 +1907,10 @@ program LudicLsp {
if k == LS_PARAM { return SC_PARAM }
if k == LS_QUERYVAR or k == LS_LOCAL or k == LS_STATE { return SC_VARIABLE }
if k == LS_UNIT { return SC_UI }
if k == LS_REGISTRY or k == LS_ENUMMEMBER { return SC_CONST }
if k == LS_ENUM { return SC_TYPE }
if k == LS_VIEW { return SC_UI }
if k == LS_REDUCER { return SC_FUNCTION }
return SC_UNKNOWN
}
function classify(D: Doc) -> void {
@ -2228,8 +2475,21 @@ program LudicLsp {
if k == LS_WIDGET { return 14 }
if k == LS_SCENE { return 3 }
if k == LS_LAYER { return 3 }
if k == LS_REGISTRY { return 18 }
if k == LS_ENUM { return 10 }
if k == LS_ENUMMEMBER { return 22 }
if k == LS_VIEW { return 5 }
if k == LS_REDUCER { return 12 }
return 13
}
# a symbol's LSP SymbolKind: a record says which it is by the word that declared it
function sym_lsp_kind(y: Sym) -> int {
if y.kind == LS_COMPONENT {
if (y.decl == "event") or (y.decl == "action") { return 24 }
if (y.decl == "port") { return 11 }
}
return lsp_symbol_kind(y.kind)
}
function comp_kind_of(k: int) -> int {
if k == LS_COMPONENT { return 22 }
if k == LS_ARCHETYPE { return 7 }
@ -2242,6 +2502,10 @@ program LudicLsp {
if k == LS_SCENE { return 11 }
if k == LS_LAYER { return 11 }
if k == LS_FIELD { return 5 }
if k == LS_REGISTRY { return 21 }
if k == LS_ENUM { return 13 }
if k == LS_ENUMMEMBER { return 20 }
if k == LS_VIEW { return 7 }
return 6
}
@ -2353,7 +2617,7 @@ program LudicLsp {
first = 0
buf_puts(o, "{\"name\":"); jstr_out(o, y.name)
buf_puts(o, ",\"detail\":"); jstr_out(o, y.detail)
buf_puts(o, ",\"kind\":"); buf_puti(o, lsp_symbol_kind(y.kind))
buf_puts(o, ",\"kind\":"); buf_puti(o, sym_lsp_kind(y))
buf_puts(o, ",\"range\":")
var a = y.start; var b = y.end
if y.bend > y.end and y.bstart >= y.start { b = y.bend }
@ -2750,7 +3014,7 @@ program LudicLsp {
if keep {
if first == 0 { buf_putc(o, ',') }
first = 0
buf_puts(o, "{\"name\":"); jstr_out(o, y.name); buf_puts(o, ",\"kind\":"); buf_puti(o, lsp_symbol_kind(y.kind)); buf_puts(o, ",\"location\":"); put_location(o, D, y.start, y.end); buf_putc(o, '}')
buf_puts(o, "{\"name\":"); jstr_out(o, y.name); buf_puts(o, ",\"kind\":"); buf_puti(o, sym_lsp_kind(y)); buf_puts(o, ",\"location\":"); put_location(o, D, y.start, y.end); buf_putc(o, '}')
}
}
k += 1

View file

@ -166,7 +166,8 @@ function sym_hover_code(O: Doc, si: int) -> pointer {
function record_hover_code(O: Doc, si: int) -> pointer {
let y = O.syms[si]
let b = buf_new()
if y.kind == LS_COMPONENT { buf_puts(b, "property ") } else { buf_puts(b, "model ") }
if not (y.decl == "") { buf_puts(b, y.decl); buf_puts(b, " ") }
else if y.kind == LS_COMPONENT { buf_puts(b, "property ") } else { buf_puts(b, "model ") }
buf_puts(b, y.name)
buf_puts(b, " {")
var n = 0
@ -224,13 +225,23 @@ function on_type_definition(msg: JVal, id: JVal) -> void {
# `model: Model` and `a.model` are all ordinary names. A word is a keyword only
# where it could start or continue a declaration.
# a keyword that may stand as a name where one is written (written by `ludic-dev syntax`)
# ludic-dev syntax: begin - generated from `ludicc --emit-syntax` (selfhost/frontend/vocab.ludic); run `ludic-dev syntax`, do not edit
function is_contextual_word(w: pointer) -> bool {
if (w == "program") or (w == "import") or (w == "property") or (w == "model") or (w == "enum") or (w == "ui") { return true }
if (w == "function") or (w == "extern") or (w == "handler") or (w == "entry") or (w == "event") or (w == "scene") { return true }
if (w == "phase") or (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true }
if (w == "state") or (w == "machine") or (w == "prefab") or (w == "namespace") or (w == "test") { return true }
if (w == "program") or (w == "module") or (w == "import") or (w == "property") or (w == "state") or (w == "event") { return true }
if (w == "action") or (w == "reducer") or (w == "enum") or (w == "model") or (w == "prefab") or (w == "scene") { return true }
if (w == "handler") or (w == "ui") or (w == "namespace") or (w == "alias") or (w == "port") or (w == "bind") { return true }
if (w == "registry") or (w == "def") or (w == "component") or (w == "view") or (w == "prop") or (w == "const") { return true }
if (w == "var") or (w == "function") or (w == "fn") or (w == "extern") or (w == "entry") or (w == "test") { return true }
if (w == "export") or (w == "internal") or (w == "friend") or (w == "uses") or (w == "open") or (w == "of") { return true }
if (w == "as") or (w == "from") or (w == "numbers") or (w == "unsafe") or (w == "mut") or (w == "phase") { return true }
if (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true }
if (w == "shows") or (w == "lasts") or (w == "then") or (w == "loads") or (w == "system") or (w == "in") { return true }
if (w == "where") or (w == "enable") or (w == "disable") or (w == "attach") or (w == "detach") or (w == "emit") { return true }
if (w == "dispatch") or (w == "try") { return true }
return false
}
# ludic-dev syntax: end
function demote_contextual_keywords(D: Doc) -> void {
let N = ntok(D)

View file

@ -48,34 +48,48 @@ typedef struct {
} LLex;
/* ---------- the vocabulary ------------------------------------------------*/
/* Reserved words, grouped so editors can colour them differently. The groups
* mirror the compiler's parser: anything parse_decl() dispatches on is a
* declaration keyword, anything stmt() dispatches on is a statement keyword. */
static const char* LUDIC_KW_DECL[] = {
"program","import","property","model","enum","ui","namespace",
"const","var","function","extern","handler","entry","event","scene","test", 0
};
static const char* LUDIC_KW_CLAUSE[] = {
"phase","query","on","cancellable","public","layer","start", 0
};
/* Keywords by role (declaring, modifying, statement), the built-in types, the
* phases and the attribute names: one vocabulary, printed by the compiler
* (`ludicc --emit-syntax`, selfhost/frontend/vocab.ludic, which is held to the
* parser's own recognisers) and written between the marked lines below by
* `ludic-dev syntax`; `ludic-dev syntax --check` fails when they fall behind. */
/* Documented design targets the self-hosted parser does not accept yet. Kept
* out of the highlighted vocabulary (they would read as working keywords) until
* they are implemented; check-vocabulary.py verifies the lists above are a
* subset of what selfhost/parse*.ludic actually dispatches on. */
* they are implemented. */
static const char* LUDIC_KW_RESERVED[] = {
0
};
/* ludic-dev syntax: begin - generated from `ludicc --emit-syntax` (selfhost/frontend/vocab.ludic); run `ludic-dev syntax`, do not edit */
static const char* LUDIC_KW_DECL[] = {
"program","module","import","property","state","event","action","reducer","enum","model",
"prefab","scene","handler","ui","namespace","alias","port","bind","registry","def","component",
"view","prop","const","var","function","fn","extern","entry","test", 0
};
static const char* LUDIC_KW_CLAUSE[] = {
"export","internal","friend","uses","open","of","as","from","numbers","unsafe","mut","phase",
"query","on","cancellable","public","layer","start","shows","lasts","then","loads","system", 0
};
static const char* LUDIC_KW_STMT[] = {
"let","return","if","else","while","for","in","spawn","despawn",
"enable","disable","match","machine","state","become","where","prefab",
"and","or","not","break","continue","new","emit","cancel","try", 0
"let","if","else","while","for","in","where","match","machine","become","return","break",
"continue","new","spawn","despawn","enable","disable","attach","detach","emit","cancel",
"dispatch","try","and","or","not", 0
};
static const char* LUDIC_TYPES[] = {
"int","long","fixed","float","double","countdown","bool","entity","string","pointer","byte","words","fixeds","floats","doubles","pointers","Vector","IVec2","Rect","void", 0
"int","long","float","double","fixed","bool","byte","string","Key","pointer","void","entity",
"countdown","words","floats","doubles","fixeds","pointers","Vector","IVec2","Rect", 0
};
static const char* LUDIC_PHASES[] = {
"Start","Input","FixedUpdate","Update","LateUpdate","Render","Overlay", 0
};
static const char* LUDIC_ATTRIBUTES[] = {
"Ref","OneOf","Range","Unit","Asset","Color","Node","Clip","Material","Tint","Derived","Text",
"Multiline","Key","Computed","Sync","frame","max","owns","AppendOnly","ByKey","PerMap",
"Chunked","TextKey","export","Public","On","Queries","OnSpawn","OnDespawn","OnAttach",
"OnDetach","OnEnable","OnDisable","OnStart","OnQuit","Owned","Server","Predicted","ToServer",
"ToClients","System","EngineSystem","Namespace","deterministic","ClearColor","alloc_ok",
"creates","releases","Handles","Reads","Writes", 0
};
/* ludic-dev syntax: end */
static const char* LUDIC_WIDGETS[] = {
"panel","col","row","label","button","image","spacer", 0
};