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:
commit
50333571d7
76 changed files with 96896 additions and 89729 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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": "[+\\-*/%]" }
|
||||
|
|
|
|||
|
|
@ -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": "[+\\-*/%]" }
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
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]) }
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue